@cahyo-dimas/freeday 3.6.0 → 3.8.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 (52) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/COMPONENTS.md +119 -6
  3. package/README.id.md +19 -6
  4. package/README.md +17 -5
  5. package/USAGE.md +42 -0
  6. package/adapters/blazor/FdyDatepicker.razor +3 -1
  7. package/adapters/blazor/FdyDatepicker.razor.cs +22 -1
  8. package/adapters/blazor/FdyTableFooter.razor +6 -6
  9. package/adapters/blazor/FdyTableFooter.razor.cs +36 -0
  10. package/adapters/react/components/FdyDatepicker.tsx +122 -42
  11. package/adapters/react/components/FdyTableFooter.tsx +37 -6
  12. package/adapters/react/index.d.ts +1 -1
  13. package/adapters/vue/components/FdyDatepicker.vue +119 -34
  14. package/adapters/vue/components/FdyTableFooter.vue +39 -6
  15. package/dist/freeday-datepicker.js +68 -14
  16. package/dist/freeday-menu.js +15 -1
  17. package/dist/freeday-popover.js +33 -11
  18. package/dist/freeday-tabs.js +21 -2
  19. package/dist/freeday.bundle.css +493 -117
  20. package/dist/freeday.css +190 -113
  21. package/dist/freeday.js +137 -28
  22. package/dist/freeday.tokens.css +303 -4
  23. package/docs/getting-started.md +1 -1
  24. package/package.json +2 -2
  25. package/src/base.css +1 -1
  26. package/src/components/accordion.css +1 -1
  27. package/src/components/alert.css +5 -5
  28. package/src/components/app-shell.css +2 -2
  29. package/src/components/appbar.css +3 -3
  30. package/src/components/avatar.css +15 -9
  31. package/src/components/badge.css +18 -14
  32. package/src/components/breadcrumb.css +1 -1
  33. package/src/components/button.css +21 -13
  34. package/src/components/carousel.css +3 -3
  35. package/src/components/cascade.css +4 -4
  36. package/src/components/chip.css +33 -15
  37. package/src/components/combo.css +5 -5
  38. package/src/components/datepicker.css +8 -8
  39. package/src/components/file-upload.css +9 -4
  40. package/src/components/input-group.css +2 -2
  41. package/src/components/input.css +2 -2
  42. package/src/components/menu.css +3 -3
  43. package/src/components/pagination.css +1 -1
  44. package/src/components/selection.css +2 -2
  45. package/src/components/skeleton.css +1 -1
  46. package/src/components/stepper.css +1 -1
  47. package/src/components/table.css +12 -7
  48. package/src/components/tabs.css +32 -1
  49. package/src/components/timepicker.css +3 -3
  50. package/src/components/toast.css +1 -1
  51. package/src/components/tree.css +1 -1
  52. package/tokens/tokens.json +57 -4
package/CHANGELOG.md CHANGED
@@ -9,6 +9,145 @@ 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.8.0] - 2026-10-23
13
+
14
+ **Empat penambahan dari catatan yang sama, dan tiga di antaranya ternyata sudah separuh ada di
15
+ kit.** App ronda yang menemukan Safari 15 di 3.7.0 menulis delapan butir; dua di antaranya cacat,
16
+ dan keduanya sudah terbit. Empat dari yang tersisa penambahan, dan keempatnya lebih murah daripada
17
+ tampaknya: satu kelas yang sudah ada sejak lama tetapi kehilangan pengaruhnya, satu jalan keluar
18
+ teks yang cuma dipasang di satu jalur, satu grid yang terpasang sejak 1.32.0 tanpa pernah bisa jadi
19
+ tujuan, dan satu varian sumbu yang papan tombolnya kurang dua literal.
20
+
21
+ Yang terakhir membuka cacat yang digambarkan catatan itu tanpa pelapornya tahu ia sedang
22
+ menggambarkan kit. Ia menulis bahwa `<input type="month">` menamai bulan menurut bahasa peramban,
23
+ bukan `<html lang>`. Kalimat itu berlaku kata per kata untuk `FdyDatepicker` Vue dan React: dokumen
24
+ sudah menjanjikan `<html lang>` sejak lama, cuma enhancer vanilla yang betul melakukannya, dan
25
+ kedua adapter menyerahkan `props.locale` yang `undefined` langsung ke `Intl`.
26
+
27
+ ### Fixed
28
+ - **Popover memposisikan panelnya dari tata letak yang ia batalkan tiga baris kemudian** (`#079`),
29
+ ada sejak 1.4.1 dan kena keenam enhancer yang memakai `attach()`: autocomplete, cascade, combo,
30
+ datepicker, menu dan timepicker. Keenam panel itu `position:absolute` dengan
31
+ `top:calc(100% + var(--space-1))` di stylesheet komponennya, dan aturan author mengalahkan gaya
32
+ UA popover, jadi panel yang baru di-`show()` sudah di top layer sambil masih memakai aturan itu.
33
+ Di top layer `100%` adalah tinggi viewport. Selama satu fungsi sinkron panelnya duduk satu
34
+ viewport di bawah halaman dan overflow dokumen tumbuh mengikutinya, terukur `scrollHeight` 1023
35
+ lawan viewport 900. Di tempat scrollbar memakan ruang tata letak, yakni setiap peramban Windows
36
+ dan Linux, dokumen mendapat scrollbar untuk sesaat, `clientWidth` menyusut selebar scrollbar itu,
37
+ halaman relayout, dan pemicunya bergeser bersamanya. `place()` membaca rect pemicu sebagai hal
38
+ pertama yang ia lakukan, jadi yang ia ukur adalah tata letak sementara itu. Pengukurannya
39
+ sekarang terjadi sesudah panel keluar dari flow.
40
+
41
+ macOS memakai scrollbar overlay yang tidak memakan ruang, jadi mesin tempat kit ini ditulis
42
+ secara struktural tidak bisa melihat cacat ini, dan 1.4.1 sampai 3.7.0 mengirimnya tanpa ada yang
43
+ tahu. Yang melihatnya runner Linux di CI, lewat uji perataan `#074` yang kebetulan mengukur
44
+ koordinat di sisi kanan layar. Reproduksinya sekarang ada di suite dan berjalan di macOS juga,
45
+ dengan menata `::-webkit-scrollbar` ke lebar tetap, plus assertion yang menolak lulus kalau
46
+ enginenya ternyata tidak memberi scrollbar ruang tata letak.
47
+
48
+ ### Added
49
+ - **`align` di popover, dibacakan dari `.fdy-menu--end`** (`#074`). Kelasnya sudah ada sejak lama
50
+ dan stylesheet selalu menghormatinya lewat `left:auto;right:0`. Jalur popover menggantinya dengan
51
+ koordinat `fixed` inline, jadi kelas itu kehilangan pengaruh persis pada saat menu masuk top
52
+ layer: menu akun di talang kanan melewati avatarnya dan berhenti 4px dari tepi layar. Nol kelas
53
+ baru. `freeday-menu.js` yang membaca kelasnya, sebab popover dipakai enam enhancer dan tidak
54
+ boleh tahu nama kelas satu pun. Jepitan viewport sekarang dua sisi: lantai `GAP` yang lama duduk
55
+ di dalam cabang jepitan kanan dan tak pernah berjalan untuk panel rata-kanan, jadi rata-kanan di
56
+ layar sempit cuma akan menukar luber kanan dengan luber kiri.
57
+ - **Enam prop teks di `FdyTableFooter`** (`#075`), di ketiga adapter. `data-fdy-text-<key>` sudah
58
+ lama membuka jalur mentah dan jalur bertipe tidak punya padanannya, jadi app yang tidak berbahasa
59
+ Inggris hanya bisa menulis ulang seluruh pita kakinya. Satu app mengirim 81 baris markup sendiri
60
+ untuk mengganti lima kata. Namanya mengikuti kunci `TEXT` enhancer di mana kuncinya ada (`info`,
61
+ `prev`, `next`), supaya satu teks tidak punya dua nama di dua jalur; tiga sisanya tanpa kembaran
62
+ sebab di jalur mentah markup itu milik app sendiri. Bawaan tetap Inggris di setiap jalur.
63
+ `FdyTable` masih merender pita kakinya sendiri tanpa meneruskan keenamnya; jalan keluarnya
64
+ `pager={false}` lalu render `<FdyTableFooter>` sendiri, dan COMPONENTS.md menuliskannya.
65
+ - **`mode="month"` di `FdyDatepicker`** (`#076`), di ketiga adapter dan jalur mentah. Grid bulannya
66
+ sudah dipasang 1.32.0, hanya tak pernah bisa jadi tujuan. Menjawab `YYYY-MM`, menerima `min` dan
67
+ `max` dalam bentuk yang sama, dan berhenti di grid itu alih-alih turun ke hari. Nama acara dan
68
+ `detail.date` tidak berubah.
69
+ - **`.fdy-tabs--vertical`, dan sumbu papan tombol dari `aria-orientation`** (`#077`). Kelasnya
70
+ memutuskan tampilan, atributnya memutuskan panah, dan satu penanda cukup: enhancer mengisi
71
+ atribut dari kelas kalau markup belum menulisnya, sementara atribut yang sudah ditulis selalu
72
+ menang. Panah kiri dan kanan tidak dipetakan di tegak, mengikuti APG satu sumbu per tablist, dan
73
+ tombol di luar sumbu jatuh tanpa `preventDefault` sehingga tetap milik halaman. Lebar kolom lewat
74
+ `--fdy-tabs-list-w`, bawaan `auto`. Tabs tidak punya wrapper bertipe di stack mana pun, jadi tidak
75
+ ada prop `orientation` yang bisa ditambahkan dan tidak ada wrapper baru yang dibuat demi punya
76
+ tempat menaruhnya.
77
+
78
+ ### Changed
79
+ - **`FdyDatepicker` Vue dan React membaca `<html lang>` kalau `locale` tidak dikirim** (`#078`).
80
+ Ini satu-satunya perubahan perilaku di rilis ini, dan ia kena mode hari juga, bukan cuma mode
81
+ bulan. Sebelumnya `undefined` sampai ke `Intl` apa adanya, yang berarti bahasa peramban: halaman
82
+ Indonesia yang dibuka di peramban Inggris menulis nama bulan Inggris, dan halaman Inggris di
83
+ peramban Indonesia menulis nama bulan Indonesia. Kontrak yang tertulis selalu `<html lang>`, dan
84
+ aturan "Inggris di semua jalur" tidak bisa ditegakkan oleh nilai yang datang dari preferensi
85
+ pembaca. App yang memang menginginkan bahasa peramban mengirim `locale` sendiri. Halaman tanpa
86
+ `lang` jatuh ke `en`, dan `lang=""` ikut ke sana sebab `Intl` melempar `RangeError` atas string
87
+ kosong.
88
+
89
+ ### Catatan dukungan peramban
90
+ Floor-nya tetap Chrome 105, Safari 15.4 dan Firefox 121. Tab tegak sengaja tidak memakai `:has()`
91
+ walau itu akan memendekkan CSS-nya, sebab `:has()` baru ada di Safari 15.4 sementara lantai yang
92
+ pernah dilaporkan dari perangkat nyata adalah iOS 15.0.
93
+
94
+ ### Yang perlu dibuang app saat upgrade
95
+ - Tambalan `translate` apa pun yang menggeser menu rata-kanan ke dalam talangnya. Menu itu sekarang
96
+ diposisikan benar, jadi tambalannya menggeser sejauh satu talang ke arah yang salah.
97
+ - Markup pita kaki tabel yang ditulis tangan cuma karena teksnya tak bisa diganti, berikut CSS
98
+ lokal yang menemaninya.
99
+
100
+ ### Belum masuk
101
+ Butir keempat catatan itu, bawaan `.fdy-badge` yang berwarna primer sehingga status netral terbaca
102
+ seperti aksi, menunggu 4.0: mengubah bawaannya mengubah tampilan setiap lencana yang sudah terkirim.
103
+ Pilihan yang tidak mematahkan apa pun, `fdy-badge--neutral` sebagai opt-in, masih terbuka lebih awal.
104
+
105
+ ## [3.7.0] - 2026-10-23
106
+
107
+ **Kit ini tidak bisa dipakai di Safari 15, dan yang menemukannya adalah sebuah HP.** Pemilik sebuah
108
+ app ronda membuka aplikasinya di iPhone 7 dan menulis tiga kata: tampilannya ancur banget. Sebabnya
109
+ `color-mix()`, 94 kemunculan tanpa satu pun cadangan, dipakai untuk `box-shadow` (26),
110
+ `border-color` (17), `color` (11) dan `background` (5). Safari baru mendukungnya di 16.2; iPhone 7
111
+ mentok di iOS 15.
112
+
113
+ Perbaikan pertama yang dicoba justru membuktikan sebabnya. Pola cadangan yang biasa dipakai orang,
114
+ nilai statis dulu lalu `color-mix()` sesudahnya, tidak bekerja ketika deklarasinya memuat `var()`:
115
+ deklarasi itu sah saat parse dan baru gagal saat computed-value, dan di jalur itu properti jatuh ke
116
+ nilai initial, bukan ke baris sebelumnya. Untuk `border-color` nilai initial itu `currentColor`,
117
+ yaitu hitam. Keluhan tentang garis hitam tebal di mana-mana itulah bukti mekanismenya. Jadi
118
+ `color-mix()`-nya yang dibuang.
119
+
120
+ ### Fixed
121
+ - **Nol `color-mix()` di seluruh CSS yang dikirim** (`#072`). `rgba(var(--x-rgb), .p)` identik
122
+ dengan `color-mix(in srgb, X p%, transparent)` menurut definisi, jadi 10 token semantik baru
123
+ menutup 84 dari 94 kemunculan; `--color-primary-rgb` sendirian menutup 45. Sisanya yang mencampur
124
+ terhadap permukaan dipipihkan di `tokens/build.mjs`, yang kini menghitung nilai bertulis
125
+ `color-mix(in srgb, {a} p%, {b})` sekali per tema sambil menelusuri rantai `$dark ?? $value`,
126
+ sehingga nilai gelapnya diturunkan alih-alih diketik dua kali.
127
+ - **Modifier tone bekerja di Safari 15** (`#072`). Delapan tone kali latar, tinta dan dua garis kini
128
+ 33 token terhitung, dan properti privat `--_fdy-{avatar,badge,chip}-tone` hilang seluruhnya.
129
+ Tampilan di peramban modern terbukti tidak berubah: byte yang benar-benar dicat dibandingkan lewat
130
+ tangkapan layar satu piksel, nol selisih di 48 warna kali dua tema.
131
+ - **Lima tempat yang mencetak teks dengan `--color-primary` pindah ke `--color-primary-strong`**
132
+ (`#073`). Kontrak kontras hanya pernah menjanjikan peran itu terhadap permukaan pada ambang 3:1,
133
+ yakni ambang WCAG untuk komponen UI, bukan untuk teks. Jadi suite-nya lulus sambil kit mencetak
134
+ tinta di bawah AA pada teal 3.74, cyan 3.68, emerald 3.77 dan lime 3.09. Azure lulus di 6.56, dan
135
+ itu sebabnya tidak ada yang menyadarinya. Yang pindah: tautan, centang combo, ikon dropzone saat
136
+ seret, dan dua panah urut tabel. Sembilan belas tempat lain yang polanya ikut tertangkap sengaja
137
+ tidak disentuh, sebab semuanya batas kontrol dan objek grafis yang memang sah di 3:1.
138
+
139
+ ### Added
140
+ - **Penjaga yang membuat ketiganya tidak bisa kembali diam-diam.** Satu uji menegaskan nol
141
+ `color-mix()` di CSS yang dikirim. Satu lagi menegaskan nol properti `color` yang menyebut
142
+ `--color-primary` gundul, membaca nama properti alih-alih substring supaya kesembilan belas isian
143
+ tetap sah. Dan kontrak kontras kini mengukur `--color-primary-strong` terhadap permukaan pada
144
+ ambang teks untuk setiap palet, bukan hanya azure.
145
+
146
+ ### Catatan dukungan peramban
147
+ Floor-nya kini Chrome 105, Safari 15.4 dan Firefox 121, ditentukan `:has()`. Angka itu diaudit dari
148
+ fitur yang dipakai, bukan dari sesi di perangkat sungguhan. Belum ada yang menjalankan kit ini di
149
+ Safari 15.4 nyata.
150
+
12
151
  ## [3.6.0] - 2026-10-22
13
152
 
14
153
  **Tiga belas hal yang dibangun ulang sebuah app back-office karena kit belum punya.** App-nya tidak
package/COMPONENTS.md CHANGED
@@ -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'` | `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"`. |
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. |
@@ -1013,7 +1025,26 @@ months, `Home`/`End` January/December, `PageUp`/`PageDown` a year, `Enter`/`Spac
1013
1025
  year grid likewise: arrows ±1/±3 years, `Home`/`End` the ends of the page, `PageUp`/`PageDown` a
1014
1026
  page of twelve.
1015
1027
 
1016
- - Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
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.
1033
+
1034
+ This exists because `<input type="month">` names the month in the **browser's** language, which an
1035
+ Indonesian page read on an English phone cannot override. The month cells, the cell's accessible
1036
+ name and the value in the trigger all go through `Intl` with the page's `<html lang>`, the same
1037
+ 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.
1045
+
1046
+ - Attributes: `data-value="YYYY-MM-DD"`, `data-mode="month"`, `data-placeholder`, `data-label`,
1047
+ `data-fdy-no-icon`
1017
1048
 
1018
1049
  ```html
1019
1050
  <div class="fdy-field">
@@ -1021,6 +1052,11 @@ page of twelve.
1021
1052
  <div data-fdy-datepicker data-value="2026-07-21" data-label="Invoice date"></div>
1022
1053
  </div>
1023
1054
 
1055
+ <div class="fdy-field">
1056
+ <span class="fdy-label">Reporting month</span>
1057
+ <div data-fdy-datepicker data-mode="month" data-value="2026-10"></div>
1058
+ </div>
1059
+
1024
1060
  <div class="fdy-daterange" data-fdy-daterange role="group" aria-label="Reporting period">
1025
1061
  <div data-fdy-datepicker data-role="from" data-placeholder="From" data-label="Start date"></div>
1026
1062
  <span class="fdy-daterange__sep" aria-hidden="true">–</span>
@@ -1440,6 +1476,28 @@ render it once, outside both, with `pager={false}` on the table.
1440
1476
  | `page` | `FdyPageState` = `{ index, size, total }` | The page being shown. `size` drives both the range text and the rows-per-page control's value. |
1441
1477
  | `onPageChange?` | `(page: FdyPageState) => void` | React. A page click or a size change. Vue emits `update:page`; Blazor `PageChanged`. |
1442
1478
  | `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. |
1479
+ | `infoText?` | `string` | The range line, with `{from}`, `{to}` and `{total}` substituted. Default `Showing {from}–{to} of {total}`, the enhancer's `info` string. |
1480
+ | `pageSizeLabel?` | `string` | The word beside the rows-per-page control, which also names that control for a screen reader. Default `Rows`. |
1481
+ | `paginationLabel?` | `string` | Accessible name of the pager landmark. Default `Pagination`. |
1482
+ | `prevLabel?` | `string` | Accessible name of the previous-page arrow. Default `Previous page`. |
1483
+ | `nextLabel?` | `string` | Accessible name of the next-page arrow. Default `Next page`. |
1484
+ | `pageLabel?` | `string` | Accessible name of a numbered page button, with `{n}` substituted. Default `Go to page {n}`. |
1485
+
1486
+ **Saying it in another language.** The raw path has had `data-fdy-text-<key>` since 1.39.0 and this
1487
+ one had nothing, so a Vue, React or Blazor app that does not speak English could only rewrite the
1488
+ band by hand. One did: eighty one lines of its own markup, to change five words. The six string
1489
+ props above are the hatch that was missing. Where the enhancer already keys a string the prop
1490
+ carries that key's name, `infoText` for `info`, `prevLabel` for `prev`, `nextLabel` for `next`,
1491
+ and the same `{from}` `{to}` `{total}` `{n}` substitution, every occurrence replaced, so an app on
1492
+ both paths writes one wording once. The other three have no enhancer twin because on the raw path
1493
+ that markup is the app's own: the word beside the rows-per-page control, the pager landmark's name,
1494
+ and the name of a numbered page button. It is `pageSizeLabel` and not `rows`, since the enhancer's
1495
+ `rows` key is the table's `{n} rows` count, a different string.
1496
+
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.
1443
1501
 
1444
1502
  **Who draws the pager.** The table renders its own footer (range + pager) whenever there is more
1445
1503
  than one page. Two ways to take it over:
@@ -1832,10 +1890,15 @@ record. A crumb for a menu group that has no page of its own is plain text in th
1832
1890
  to nowhere.
1833
1891
 
1834
1892
  ## Tabs — `.fdy-tabs`
1835
- APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1893
+ APG tabs: arrows, Home/End, roving tabindex. Needs `freeday-tabs.js`.
1836
1894
  `.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
1837
1895
  so the same look serves routed sub-navigation built from plain links. See the note under Nav.
1838
1896
 
1897
+ **Which arrows move the selection** comes from the tablist's `aria-orientation`: Left and Right by
1898
+ default, Up and Down when it says `vertical`. One axis per tablist, the way APG maps it. The other
1899
+ pair stays unmapped, so Left and Right over a column of tabs still reach whatever else on the page
1900
+ wants them. Home and End answer either way.
1901
+
1839
1902
  > **The strip's line is an inset `box-shadow`, not a `border-bottom`** (`#052`; through 3.1.0 it
1840
1903
  > was a border, so an app upgrading past that release changes the override with it). To
1841
1904
  > remove it — a strip inside a header that draws its own rule — override `box-shadow:none`;
@@ -1857,6 +1920,56 @@ so the same look serves routed sub-navigation built from plain links. See the no
1857
1920
  </div>
1858
1921
  ```
1859
1922
 
1923
+ ### Vertical — `.fdy-tabs--vertical`
1924
+
1925
+ The same tabs turned down the page: the list on the leading side, the panel beside it. For a
1926
+ settings screen whose sections are a list, not a strip.
1927
+
1928
+ ```html
1929
+ <div class="fdy-tabs fdy-tabs--vertical" data-fdy-tabs style="--fdy-tabs-list-w:16rem">
1930
+ <div class="fdy-tabs__list" role="tablist" aria-orientation="vertical" aria-label="Settings">
1931
+ <button class="fdy-tabs__tab" role="tab" id="s-1" aria-controls="sp-1" aria-selected="true">Identity</button>
1932
+ <button class="fdy-tabs__tab" role="tab" id="s-2" aria-controls="sp-2" aria-selected="false" tabindex="-1">Posts</button>
1933
+ </div>
1934
+ <div class="fdy-tabs__panel" role="tabpanel" id="sp-1" aria-labelledby="s-1" tabindex="0">…</div>
1935
+ <div class="fdy-tabs__panel" role="tabpanel" id="sp-2" aria-labelledby="s-2" tabindex="0" hidden>…</div>
1936
+ </div>
1937
+ ```
1938
+
1939
+ - **Two markers, and they are not the same marker.** `.fdy-tabs--vertical` decides the look,
1940
+ because the two-column box belongs to `.fdy-tabs` and a stylesheet cannot reach a parent from a
1941
+ child. `aria-orientation="vertical"` on the tablist decides the keyboard, because that is what an
1942
+ assistive technology announces and APG asks for it on a vertical tablist regardless. Write
1943
+ **either** one: `freeday-tabs.js` fills in the attribute when it finds the class without it, so
1944
+ the ARIA can never drift from the layout. An `aria-orientation` you wrote always wins, including
1945
+ when you write `horizontal` on a widget that carries the class.
1946
+ - **Selection is a soft fill.** A bar down the leading edge would be the wrong translation:
1947
+ sideways the marker is an underline beneath the tab it belongs to, and turned ninety degrees that
1948
+ line lands between two tabs and points at the wrong one. So the vertical variant spends
1949
+ `--color-primary-soft` behind `--color-primary-strong`, the same pair
1950
+ `.fdy-nav__item[aria-current="page"]` and the picker options already use for “this is where you
1951
+ are” in a vertical list. The strip's own line (`box-shadow`) is dropped with the underline.
1952
+ - **Column width:** `--fdy-tabs-list-w` on `.fdy-tabs` (default `auto`, so the column hugs the
1953
+ longest label). Set the token rather than a `width` on `.fdy-tabs__list`: the list is a flex item
1954
+ at `flex:none`, so a width arriving from outside has no effect and no error to show for it.
1955
+ - **It does not collapse on a narrow screen**, and the breakpoint is yours because the kit cannot
1956
+ know whether these tabs span the viewport or sit in a 20rem drawer. Going back to a strip takes
1957
+ three declarations, not one, since the variant turns both boxes:
1958
+
1959
+ ```css
1960
+ @media (max-width: 63.9375rem) {
1961
+ .settings-tabs.fdy-tabs--vertical { display: block; }
1962
+ .settings-tabs .fdy-tabs__list { flex-direction: row; width: auto; overflow-x: auto; }
1963
+ .settings-tabs .fdy-tabs__panel { padding: var(--space-4) 0 0; }
1964
+ }
1965
+ ```
1966
+
1967
+ The keyboard stays on Up and Down across that switch, because `aria-orientation` is an attribute
1968
+ and a media query cannot reach it. A list of sections is still a list of sections at 320px, which
1969
+ is why the axis is read from the attribute and never from the painted layout.
1970
+ - Measured in `browser/tabs-vertical.mjs`, which also holds the line that a strip with no
1971
+ `aria-orientation` behaves and paints exactly as it did before the variant existed.
1972
+
1860
1973
  ---
1861
1974
 
1862
1975
  # Overlays
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.6.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.6.0)
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)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -255,11 +255,24 @@ terlihat, HTML native dulu sebelum ARIA, komponen interaktif ikut pola WAI-ARIA
255
255
  `docs/index.html`.
256
256
 
257
257
  ## Dukungan browser
258
- Butuh browser evergreen, kira-kira 2023+: **Chrome 111 · Safari 16.4 · Firefox 113** (floor
259
- ditentukan `color-mix()`). Fitur lain: native `<dialog>`/`::backdrop`, `accent-color`, `100dvh`,
260
- `conic-gradient`, `scroll-snap`, semuanya ≥ Safari 15.4. Blur backdrop pakai
261
- `-webkit-backdrop-filter` + `backdrop-filter` (Safari lama tetap jalan, hanya tanpa blur). Belum ada
262
- CSS build/autoprefixer, jadi dukungan browser lama jadi tanggung jawab konsumen.
258
+ **Chrome 105 · Safari 15.4 · Firefox 121.** Floor-nya ditentukan `:has()`, dan oleh generasi Safari
259
+ 15.4 untuk native `<dialog>`/`::backdrop`, `accent-color`, dan `100dvh`. `conic-gradient` dan
260
+ `scroll-snap` lebih tua lagi.
261
+
262
+ Dulu floor itu ditentukan `color-mix()` di Safari 16.4, dan sekarang tidak lagi. Harganya memang tak
263
+ pernah pantas: deklarasi yang mencampur sebuah token membawa `var()`, jadi ia dianggap sah saat
264
+ parse dan baru gagal saat computed-value, dan di jalur itu properti jatuh ke nilai *initial*-nya,
265
+ bukan ke baris cadangan di atasnya. Di iPhone yang mentok di iOS 15, 17 deklarasi `border-color`
266
+ berubah jadi `currentColor`, yaitu garis hitam tebal di hampir segalanya. Semua alpha di kit ini
267
+ sekarang ditulis `rgba(var(--color-primary-rgb), .26)`, warnanya sama dan sudah bisa diparse sejak
268
+ Safari 9; empat aturan yang memang tak bisa dijawab secara statis membawa pasangan `@supports` yang
269
+ sungguhan. `test/color-mix.test.mjs` menjaga agar yang berikutnya tetap jujur.
270
+
271
+ Yang paling baru dipakai kit ini (`@container`, `@starting-style`, `text-wrap`) sifatnya peningkatan:
272
+ browser yang belum punya akan melewati bloknya dan tetap memakai aturan tanpa syarat di bawahnya.
273
+ Blur backdrop pakai `-webkit-backdrop-filter` + `backdrop-filter` (Safari lama tetap jalan, hanya
274
+ tanpa blur). Belum ada CSS build/autoprefixer, jadi dukungan di bawah floor itu jadi tanggung jawab
275
+ konsumen.
263
276
 
264
277
  ## Lisensi
265
278
  [MIT](LICENSE) © 2026 Cahyo D. Kurnianto. Bebas dipakai, diubah, dan didistribusikan asal
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.6.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.6.0)
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)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -264,11 +264,23 @@ patterns, and `prefers-reduced-motion` is respected. Status is never conveyed by
264
264
  the "Accessibility" section in `docs/index.html`.
265
265
 
266
266
  ## Browser support
267
- Needs an evergreen browser, roughly 2023+: **Chrome 111 · Safari 16.4 · Firefox 113** (the floor is
268
- set by `color-mix()`). Other features used: native `<dialog>`/`::backdrop`, `accent-color`,
269
- `100dvh`, `conic-gradient`, `scroll-snap`, all of them ≥ Safari 15.4. Backdrop blur uses
267
+ **Chrome 105 · Safari 15.4 · Firefox 121.** The floor is set by `:has()`, and by the Safari 15.4
268
+ generation of native `<dialog>`/`::backdrop`, `accent-color` and `100dvh`. `conic-gradient` and
269
+ `scroll-snap` are older still.
270
+
271
+ `color-mix()` used to set that floor at Safari 16.4, and it no longer does. It was never worth it:
272
+ a declaration that mixes a token carries `var()`, so it is assumed valid at parse time and only
273
+ fails at computed-value time, where the property drops to its *initial* value instead of to a
274
+ fallback line. On an iPhone that stopped at iOS 15 that turned 17 `border-color` declarations into
275
+ `currentColor`, which is a thick black outline on roughly everything. Every alpha in the kit is now
276
+ written `rgba(var(--color-primary-rgb), .26)`, which is the same colour and parses back to Safari 9;
277
+ the four rules that cannot be answered statically carry a real `@supports` counterpart.
278
+ `test/color-mix.test.mjs` keeps the next one honest.
279
+
280
+ The newest things the kit touches (`@container`, `@starting-style`, `text-wrap`) are enhancements: a
281
+ browser without them skips the block and keeps the unconditional rule underneath. Backdrop blur uses
270
282
  `-webkit-backdrop-filter` + `backdrop-filter` (older Safari still works, just without the blur).
271
- There's no CSS build or autoprefixer step, so supporting older browsers is the consumer's
283
+ There's no CSS build or autoprefixer step, so supporting anything below the floor is the consumer's
272
284
  responsibility.
273
285
 
274
286
  ## License
package/USAGE.md CHANGED
@@ -107,6 +107,48 @@ point, everything around it quiet.
107
107
  - **Surfaces:** most backgrounds are `--color-surface`; `--color-surface-2`/`-3` for a recessed area;
108
108
  `--color-primary-soft` only when you want a tinted callout, not as a default panel colour.
109
109
 
110
+ ### Putting an alpha on a role: use the channel triple, not `color-mix()`
111
+
112
+ Every role you are likely to fade has a companion triple, so a 26% primary wash is:
113
+
114
+ ```css
115
+ background: rgba(var(--color-primary-rgb), .26);
116
+ ```
117
+
118
+ Reach for `color-mix(in srgb, var(--color-primary) 26%, transparent)` instead and you get the same
119
+ colour on your machine and a missing one on an older iPhone, with **no way to put a fallback under
120
+ it.** A declaration that mentions a custom property is assumed valid when the stylesheet is parsed
121
+ and is only checked later, when the variable is substituted; if `color-mix()` is unknown at that
122
+ point the declaration is thrown away *there*, and the browser does not go back to the static line
123
+ you wrote above it. The property takes its initial value instead, which for `border-color` means
124
+ `currentColor`: a thick black outline. Safari learned `color-mix()` in 16.2, so every phone that
125
+ stopped at iOS 15 sees that.
126
+
127
+ The triples are generated from the tokens they are named after, in all 18 palettes and both themes,
128
+ so they follow a re-brand on their own. The ones that ship: `--color-primary-rgb`,
129
+ `--color-on-primary-rgb`, `--color-danger-rgb`, `--color-danger-btn-rgb`, `--color-success-rgb`,
130
+ `--color-warning-rgb`, `--color-info-rgb`, `--color-surface-rgb` and `--color-inverse-text-rgb`.
131
+ If you need one the kit does not publish, ask for it rather than hard-coding channels.
132
+
133
+ ### Tinting one role over another: the colour is already a token
134
+
135
+ A triple puts an alpha on **one** role. It cannot say "18% of this tone over the surface", because
136
+ the result depends on two roles and on the theme. Those blends are computed when the kit is built
137
+ and published as flat tokens, so you never write the mix at all:
138
+
139
+ ```css
140
+ background: var(--tone-3-bg); /* 18% of --tone-3 over --color-surface */
141
+ color: var(--tone-3-ink); /* 50% of --tone-3 toward --color-text */
142
+ border-color: var(--tone-3-border);/* 30% of --tone-3 over --color-border */
143
+ ```
144
+
145
+ `--tone-N-bg` / `-ink` / `-border` / `-border-soft` exist for all eight tones in both themes, and are
146
+ exactly what `.fdy-avatar--tone-N`, `.fdy-badge--tone-N` and `.fdy-chip--tone-N` paint — the
147
+ `-ink` on the `-bg` is the pair the WCAG gate measures, so a custom element built from the same two
148
+ tokens inherits that guarantee. `--color-danger-border` (40% of danger over the border) is the same
149
+ idea for a row that failed. If you need a blend the kit does not publish, ask for it: it belongs in
150
+ `tokens/tokens.json`, where the build can resolve it down both themes, not in a component.
151
+
110
152
  ## 5b. Theme: global by default, per-subtree when a region is inverted
111
153
 
112
154
  `data-theme="light|dark"` redefines the semantic tokens. Set it on `<html>` and it themes the app;
@@ -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"
@@ -5,11 +5,22 @@ namespace Freeday.Blazor;
5
5
 
6
6
  public partial class FdyDatepicker
7
7
  {
8
- /// <summary>Selected date as an ISO string (<c>yyyy-MM-dd</c>), or "" when unset.
8
+ /// <summary>Selected date as an ISO string (<c>yyyy-MM-dd</c>), or <c>yyyy-MM</c> when
9
+ /// <see cref="Mode"/> is <see cref="FdyDatepickerMode.Month"/>, or "" when unset.
9
10
  /// Two-way bindable via <c>@bind-Value</c>.</summary>
10
11
  [Parameter] public string? Value { get; set; }
11
12
  [Parameter] public EventCallback<string?> ValueChanged { get; set; }
12
13
 
14
+ /// <summary>What the picker answers with. <see cref="FdyDatepickerMode.Day"/> (default) is the
15
+ /// calendar as it always was. <see cref="FdyDatepickerMode.Month"/> opens on the month grid,
16
+ /// stops there, and reports <c>yyyy-MM</c>.</summary>
17
+ [Parameter] public FdyDatepickerMode Mode { get; set; } = FdyDatepickerMode.Day;
18
+
19
+ /* null rather than "day" for the default: an attribute the enhancer does not need is an
20
+ 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. */
22
+ private string? ModeAttribute => Mode == FdyDatepickerMode.Month ? "month" : null;
23
+
13
24
  /// <summary>Accessible label for the trigger.</summary>
14
25
  [Parameter] public string? Label { get; set; }
15
26
  [Parameter] public string? Placeholder { get; set; }
@@ -83,3 +94,13 @@ public partial class FdyDatepicker
83
94
  /// <summary>The detail shape of the enhancer's <c>fdy-datepicker-change</c> event.</summary>
84
95
  public sealed record ChangeDetail(string? Value, string? Date);
85
96
  }
97
+
98
+ /// <summary>What <see cref="FdyDatepicker"/> answers with.</summary>
99
+ public enum FdyDatepickerMode
100
+ {
101
+ /// <summary>A single day, reported as <c>yyyy-MM-dd</c>.</summary>
102
+ Day,
103
+
104
+ /// <summary>A whole month, reported as <c>yyyy-MM</c>.</summary>
105
+ Month,
106
+ }
@@ -9,12 +9,12 @@
9
9
  @if (Visible)
10
10
  {
11
11
  <div class="fdy-table-footer">
12
- <span class="fdy-table-footer__info">Showing @RangeFrom–@RangeTo of @Page.Total</span>
12
+ <span class="fdy-table-footer__info">@InfoLine</span>
13
13
 
14
14
  @if (Sizes.Count > 0)
15
15
  {
16
16
  <div class="fdy-table-footer__size">
17
- <span id="@SizeLabelId">Rows</span>
17
+ <span id="@SizeLabelId">@(PageSizeLabel ?? "Rows")</span>
18
18
  @* The kit's own listbox, not a native <select>: an OS menu is unthemeable, and on
19
19
  macOS it drops a dark panel into a light page. FdyCombo's own header says so. *@
20
20
  <FdyCombo TValue="string" Value="@Page.Size.ToString()" Options="SizeOptions"
@@ -24,10 +24,10 @@
24
24
 
25
25
  @if (HasPager)
26
26
  {
27
- <nav class="fdy-pagination" aria-label="Pagination">
27
+ <nav class="fdy-pagination" aria-label="@(PaginationLabel ?? "Pagination")">
28
28
  <ul class="fdy-pagination__list">
29
29
  <li>
30
- <button type="button" class="fdy-pagination__link" aria-label="Previous page"
30
+ <button type="button" class="fdy-pagination__link" aria-label="@(PrevLabel ?? "Previous page")"
31
31
  disabled="@(CurrentPage1 == 1)" @onclick="() => GoTo(CurrentPage1 - 1)">‹</button>
32
32
  </li>
33
33
  @foreach (int? p in Pages)
@@ -44,13 +44,13 @@
44
44
  else
45
45
  {
46
46
  int page = p.Value;
47
- <button type="button" class="fdy-pagination__link" aria-label="@($"Go to page {page}")"
47
+ <button type="button" class="fdy-pagination__link" aria-label="@PageAriaLabel(page)"
48
48
  @onclick="() => GoTo(page)">@(page)</button>
49
49
  }
50
50
  </li>
51
51
  }
52
52
  <li>
53
- <button type="button" class="fdy-pagination__link" aria-label="Next page"
53
+ <button type="button" class="fdy-pagination__link" aria-label="@(NextLabel ?? "Next page")"
54
54
  disabled="@(CurrentPage1 == TotalPages)" @onclick="() => GoTo(CurrentPage1 + 1)">›</button>
55
55
  </li>
56
56
  </ul>
@@ -21,6 +21,35 @@ public partial class FdyTableFooter
21
21
  /// </summary>
22
22
  [Parameter] public IReadOnlyList<int>? PageSizes { get; set; }
23
23
 
24
+ // The strings, six of them, one per thing this band says. The raw path has had a hatch since
25
+ // 1.39.0 (data-fdy-text-<key> read through textOf()) and this path had none, so an app that
26
+ // does not speak English could only rewrite the whole band by hand. A Blazor app mixes both
27
+ // paths by construction, since FreedayBlazor.initAll runs the enhancers, so these names follow
28
+ // the enhancer's TEXT keys where a key exists. Defaults stay English on every path, which
29
+ // COMPONENTS.md promises and the suite enforces.
30
+
31
+ /// <summary>
32
+ /// The range line. <c>{from}</c>, <c>{to}</c> and <c>{total}</c> are substituted, every
33
+ /// occurrence, as the enhancer's <c>textOf()</c> does. Default <c>Showing {from}–{to} of
34
+ /// {total}</c>, the enhancer's <c>info</c> key.
35
+ /// </summary>
36
+ [Parameter] public string? InfoText { get; set; }
37
+
38
+ /// <summary>The visible word beside the rows-per-page control, which also names it. Default <c>Rows</c>.</summary>
39
+ [Parameter] public string? PageSizeLabel { get; set; }
40
+
41
+ /// <summary>Accessible name of the pager landmark. Default <c>Pagination</c>.</summary>
42
+ [Parameter] public string? PaginationLabel { get; set; }
43
+
44
+ /// <summary>Accessible name of the previous-page arrow. Default <c>Previous page</c>.</summary>
45
+ [Parameter] public string? PrevLabel { get; set; }
46
+
47
+ /// <summary>Accessible name of the next-page arrow. Default <c>Next page</c>.</summary>
48
+ [Parameter] public string? NextLabel { get; set; }
49
+
50
+ /// <summary>Accessible name of a numbered page button. <c>{n}</c> substituted. Default <c>Go to page {n}</c>.</summary>
51
+ [Parameter] public string? PageLabel { get; set; }
52
+
24
53
  private IReadOnlyList<int> Sizes => PageSizes ?? Array.Empty<int>();
25
54
 
26
55
  // Just the number: the control sits beside a label reading "Rows", so "20 per page" states the
@@ -40,6 +69,13 @@ public partial class FdyTableFooter
40
69
 
41
70
  private bool HasPager => Page.Size > 0 && TotalPages > 1;
42
71
 
72
+ private string InfoLine => (InfoText ?? "Showing {from}–{to} of {total}")
73
+ .Replace("{from}", RangeFrom.ToString())
74
+ .Replace("{to}", RangeTo.ToString())
75
+ .Replace("{total}", Page.Total.ToString());
76
+
77
+ private string PageAriaLabel(int page1) => (PageLabel ?? "Go to page {n}").Replace("{n}", page1.ToString());
78
+
43
79
  // One page and no size control means there is nothing here to say, the table has always
44
80
  // withheld the whole band in that case, and this is where that decision now lives.
45
81
  private bool Visible => HasPager || Sizes.Count > 0;