@cahyo-dimas/freeday 3.9.4 → 3.11.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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,106 @@ 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.11.0] - 2026-10-26
13
+
14
+ **Audit kontrol native di seluruh layar sebuah aplikasi konsumen, bukan di satu layar yang
15
+ mengeluh.** Pemiliknya melaporkan tampilan HP aslinya tak cocok dengan peramban di lebar yang sama,
16
+ dan dugaan pertama -- termasuk dugaan saya -- adalah CSS. Pengukuran menyangkalnya: dengan
17
+ stylesheet aplikasi yang sebenarnya, pada 375x667, Chromium dan WebKit menata `date`, `time`,
18
+ `number`, `search` dan `text` **identik**, 293x44 keenamnya, selisih satu piksel pada `y` yang
19
+ cuma pembulatan. Jadi yang berbeda bukan tata letaknya, melainkan **widget native-nya**: `date` dan
20
+ `time` digambar sistem operasi, sehingga iOS memberi roda gulir, Android piringan jam dan desktop
21
+ sebuah stepper, masing-masing dengan lebar dan format nilainya sendiri, dan tak ada CSS yang
22
+ menjangkaunya. Itu sebabnya satu HP dan satu peramban di lebar yang sama bisa tidak sepakat soal
23
+ layar yang selebihnya sama.
24
+
25
+ Kit sudah mengatakan ini di tabelnya sendiri ("Use `.fdy-datepicker` / `.fdy-timepicker` /
26
+ `.fdy-datetimepicker` instead") dan sudah mengirim CSS plus enhancer-nya. Yang belum ada adalah
27
+ wrapper bertipe untuk timepicker, jadi aplikasi Vue/React/Blazor harus merakit sendiri
28
+ plumbing custom-event-nya -- dan merakit sendiri adalah yang membuat empat `<input type="time">`
29
+ bertahan di satu formulir shift.
30
+
31
+ Catatan kejujuran: perbedaan iOS itu **tidak** direproduksi di sini. WebKit desktop bukan Mobile
32
+ Safari, dan penampilan kontrol native memang bersifat platform; buktinya adalah tangkapan layar
33
+ perangkat nyata plus fakta terukur bahwa tata letak per lebar sudah identik. Yang dapat diuji
34
+ lokal, dan kini diuji, adalah perilaku penggantinya.
35
+
36
+ ### Added
37
+ - **`FdyTimepicker`** (`#094`), wrapper bertipe di **ketiga** stack, menjadikannya tiga belas.
38
+ Vue dan React mengimplementasikan listbox-nya secara native sebab ia kontrol form -- alasan yang
39
+ sama dengan `FdyCombo`, dan alasan yang `FdyChart` nyatakan dari sisi sebaliknya: enhancer yang
40
+ menguasai DOM berkelahi dengan render framework dan tak menyisakan `v-model`. Blazor
41
+ menjembatani enhancer lewat seed, seperti `FdyDatepicker`. Sorotannya **dijepit, bukan
42
+ berputar**: pada daftar 48 opsi, ArrowUp di `00:00` yang melompat ke `23:30` terbaca sebagai
43
+ kerusakan. Sorotan juga digulirkan ke dalam pandangan, sebab panelnya `max-height:15rem` dan
44
+ sorotan yang tak terlihat bukan sorotan.
45
+ - **`data-id` dan `data-describedby` pada seed timepicker** (`#094`). Keduanya tak terbaca
46
+ sebelumnya, jadi host yang menampilkan teks galat tak punya cara menautkannya. Pemicunya
47
+ `<button>`, yang **bukan** elemen labelable, sehingga `<label for>` memang tak akan pernah
48
+ mengaktifkannya -- itu didokumentasikan sekarang, bukan dibiarkan jadi kejutan.
49
+ - **`FreedayTimepicker.setState(root, { disabled, readonly, invalid })`** (`#094`), bentuknya sama
50
+ dengan `FreedayDatepicker.setState`. Tanpa ini, host yang merender seed-nya sekali lalu tidak
51
+ pernah lagi (Blazor, `ShouldRender => false`) memiliki tiga parameter keadaan yang tak ada yang
52
+ membacanya setelah render pertama.
53
+
54
+ ### Fixed
55
+ - **`FdyDateRange` membuang kedelapan prop label navigasi kalender** (`#095`), di ketiga stack.
56
+ `FdyDatepicker` menerimanya sejak 2.1.1; rentangnya tidak meneruskannya, sehingga rentang tanggal
57
+ adalah **satu-satunya** kontrol tanggal di kit yang navigasinya tak bisa dinamai dalam bahasa
58
+ halamannya sendiri -- aplikasi berbahasa Indonesia mendapat "Previous month" tepat di tempat yang
59
+ tak dapat ia jangkau. Diteruskan apa adanya ke kedua pemilih. Prop datar `xxxLabel`, bukan objek
60
+ `labels`, sebab bentuk itulah yang sudah dipakai setiap komponen lain di kit. Blazor menambah
61
+ `BackToMonthsLabel`, karena seed yang kedua pemilihnya dibangun dari situ punya tombol itu.
62
+
63
+ ## [3.10.0] - 2026-10-26
64
+
65
+ **Lima rilis dalam satu hari, empat di antaranya menambal satu kelas yang sama, dan pemiliknya
66
+ menghentikan polanya.** Setiap cacat Safari hari ini ditemukan oleh sebuah iPhone 7 sesudah
67
+ terkirim, satu per satu, karena suite kit berjalan di Chrome saja: `browser/harness.mjs` berbicara
68
+ CDP ke `chrome-headless-shell` dan tak ada WebKit di ujung kabel itu. Lantai dukungan di README
69
+ adalah prosa yang tak ada yang membacanya. Rilis ini memperbaiki keduanya.
70
+
71
+ ### Added
72
+ - **Gerbang lantai fitur** (`#091`), `test/baseline.test.mjs`. Ia **membaca ketiga angka lantai
73
+ dari README** alih-alih mengetiknya ulang, mengenumerasi 41 fitur CSS di kedua stylesheet yang
74
+ dikirim, dan gagal ketika satu di antaranya menuntut rilis yang lantainya tak capai. Pengecualian
75
+ boleh ada tetapi wajib membawa alasan tertulis minimal 160 karakter, DAN fiturnya harus benar-benar
76
+ masih melampaui lantai; kalau tidak gerbangnya menyuruh menghapus entrinya. Daftar pemaafan yang
77
+ tumbuh tanpa alasan adalah penjaga yang berhenti menjaga, dan itu persis kegagalan yang sudah
78
+ terjadi dua kali di hari yang sama.
79
+ - **Suite WebKit di gerbang** (`#092`), job CI tersendiri dengan Playwright, terpisah penuh dari
80
+ harness CDP yang menjalankan 206 uji dan tidak menyentuhnya. Sebelas uji menutup apa yang hanya
81
+ WebKit dapat perlihatkan: backdrop dan centang checkbox dan thumb switch harus MENCAPAI keadaan
82
+ akhirnya di bawah Reduce Motion, tap pada backdrop menutup drawer (sintesis klik `#057`), dan
83
+ asap tata letak telepon. Ia menolak lulus diam-diam lewat lima jalur yang semuanya diuji terhadap
84
+ laporan JSON nyata: laporan hilang, nol uji, ada yang dilewati, ada yang goyah, atau uji identitas
85
+ mesin hilang.
86
+
87
+ ### Fixed
88
+ - **Tiga fitur melampaui lantai yang kit nyatakan sendiri** (`#091`). `@container` di CFL dipagari
89
+ `@supports` dengan cadangan `@media (max-width:32rem)`, dan angka itu diturunkan dari lebar
90
+ dialognya sendiri bukan dipilih; absennya bukan kosmetik, sebab di Safari 15.4 sampai 15.8 seluruh
91
+ bloknya dibuang dan dialog 372px memakai tabel tiga kolom yang digulir dua arah.
92
+ `overscroll-behavior` dan `text-wrap:balance` jadi pengecualian beralasan. **Lantainya tidak
93
+ dinaikkan:** iPhone 7 yang menemukan empat cacat hari ini berhenti di iOS 15.
94
+ - **`user-select` dikirim tanpa pasangan `-webkit-`** di `.fdy-avatar` dan `.fdy-breadcrumb__sep`
95
+ (`#091`). WebKit menuntut prefiks itu sampai Safari 17.
96
+ - **Blok durasi-mungil lama di `src/base.css` mengalahkan perlakuan reduced-motion kit sendiri**
97
+ (`#093`). `*,*::before,*::after{animation-duration:.001ms !important}` sebagai `!important` pada
98
+ `*` menang atas `.fdy-spinner{animation-duration:2s}`, `.fdy-skeleton{animation:none}` dan
99
+ `.fdy-progress{animation:none}`, jadi ketiga perlakuan yang ditulis khusus untuk setelan itu tidak
100
+ pernah bekerja. Ketiganya satu-satunya animasi di luar pagar `no-preference`, jadi membuang blok
101
+ lama memulihkan semuanya tanpa membuka apa pun. Penjaga 3.9.4 melewatkannya karena regexnya
102
+ menuntut `0.001ms` berawalan nol sementara berkasnya menulis `.001ms`; sekarang ia menerima
103
+ keduanya dan ikut memeriksa `animation-duration`.
104
+
105
+ ### Docs
106
+ - **Meta viewport dinyatakan sebagai syarat**, bukan ditampilkan di contoh. Tanpanya mesin mobile
107
+ menata halaman di lebar bawaan sekitar 980px, `(max-width:720px)` tak pernah cocok, dan seluruh
108
+ lapisan responsif kit absen: shell berdiri dalam bentuk desktopnya di telepon. Itu bukan
109
+ hipotetis. Fixture tanpa tag itu melaporkan `window.innerWidth` 981 pada deskriptor iPhone 7 di
110
+ jalanan pertama suite WebKit, bentuk yang sama persis dengan laporan yang memulai suite itu.
111
+
12
112
  ## [3.9.4] - 2026-10-26
13
113
 
14
114
  **Sembilan dari lima puluh enam, dan empat puluh delapan sisanya ranjau.** Tiga rilis berturut-turut
package/COMPONENTS.md CHANGED
@@ -34,8 +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, twelve components have a typed wrapper. Use it.** `FdyCombo` ·
38
- `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` ·
37
+ 7. **On Vue, React or Blazor, thirteen components have a typed wrapper. Use it.** `FdyCombo` ·
38
+ `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyTimepicker` · `FdyAutocomplete` ·
39
+ `FdyCascade` · `FdyCfl` ·
39
40
  `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`, from `@cahyo-dimas/freeday/vue`, `/react`, or the
40
41
  `Freeday.Blazor` RCL. Each is flagged at its own section below. Hand-writing their raw markup in
41
42
  those stacks *looks* right at first, because the enhancer initialises once and the first render is
@@ -1020,6 +1021,7 @@ grid's own title.
1020
1021
  | `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
1021
1022
  | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the group (`role="group"`), or points at the text that does. |
1022
1023
  | `disabled?` · `readonly?` · `invalid?` | `boolean` | Applied to both pickers together. |
1024
+ | `prevMonthLabel?` · `nextMonthLabel?` · `prevYearLabel?` · `nextYearLabel?` · `prevYearsLabel?` · `nextYearsLabel?` · `chooseMonthLabel?` · `chooseYearLabel?` | `string` | Forwarded verbatim to **both** pickers. Until 3.11.0 this was the one picker in the kit whose calendar nav could not be named in the page's own language: the single picker took these, the range dropped them. Blazor adds `BackToMonthsLabel`, since the raw seed both pickers are built from has that button. |
1023
1025
 
1024
1026
  Blazor splits the value into two scalars, `From` / `FromChanged` and `To` / `ToChanged`, because a
1025
1027
  C# binding cannot express one object bound in two places, and it adds `Separator`.
@@ -1106,10 +1108,52 @@ it has always been, down to the `YYYY-MM-DD` it reports.
1106
1108
  ```
1107
1109
 
1108
1110
  ## Time picker — `data-fdy-timepicker`
1109
- 24-hour `HH:MM` listbox. `data-step` (minutes, default 30), `data-min`, `data-max`. Needs
1111
+ > **Typed wrapper: `<FdyTimepicker>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
1112
+
1113
+ 24-hour `HH:MM` listbox. **Author an empty `<div>`** — the enhancer builds everything. Needs
1110
1114
  `freeday-timepicker.js`. Classes: `.fdy-timepicker` (+`--error`) · `__trigger` `__value`
1111
1115
  (+`--placeholder`) `__icon` `__panel` `__opt`.
1112
1116
 
1117
+ Seed attributes: `data-step` (minutes, default 30), `data-min` (default `00:00`), `data-max`
1118
+ (default `23:59`), `data-value`, `data-label`, `data-placeholder` (default `--:--`), `data-id`,
1119
+ `data-describedby`, `data-disabled`, `data-readonly`, `data-invalid`. Emits a bubbling
1120
+ `fdy-time-select` (`detail {value}`). `FreedayTimepicker.setState(root, { disabled, readonly,
1121
+ invalid })` changes the three states afterwards, which is what a host that renders its seed once
1122
+ and never again (Blazor) needs.
1123
+
1124
+ **Use this instead of `<input type="time">`**, and not for theming alone: the native picker is
1125
+ drawn by the *operating system*, so iOS shows a scroll wheel, Android a clock dial and desktop a
1126
+ stepper, each with its own width and value format, and no CSS reaches any of them. A phone and a
1127
+ browser at the same width therefore disagree about a screen that is otherwise identical.
1128
+
1129
+ ```html
1130
+ <div data-fdy-timepicker data-step="15" data-min="06:00" data-max="22:00"
1131
+ data-value="07:00" data-label="Jam mulai"></div>
1132
+ ```
1133
+
1134
+ ### Props — `<FdyTimepicker>`
1135
+
1136
+ | Prop | Type | What it does |
1137
+ |---|---|---|
1138
+ | `modelValue` · `value` | `string \| null` | `HH:MM`, 24-hour. `''`/`null` for empty. A loose `H:MM` is normalised for matching. Blazor: `Value` / `@bind-Value`. |
1139
+ | `onChange` | `(value: string) => void` | React. Vue emits `update:modelValue` and `change`, Blazor `ValueChanged`. |
1140
+ | `step?` | `number` | Minutes between options. Default 30. |
1141
+ | `min?` · `max?` | `string` | `HH:MM` bounds, inclusive. Default `00:00` to `23:59`. |
1142
+ | `placeholder?` | `string` | Shown when there is no value. Default `--:--`. |
1143
+ | `ariaLabel?` · `ariaLabelledby?` · `describedby?` | `string` | Names the trigger, or points at the text that does. Blazor: `Label` / `Describedby`. |
1144
+ | `id?` | `string` | Id for the trigger. It is a `<button>`, which is **not** a labelable element, so a `<label for>` will not activate it — name it with `ariaLabel`/`ariaLabelledby`. |
1145
+ | `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps tab order and shows the time but will not open. |
1146
+
1147
+ Keyboard: `ArrowDown`/`ArrowUp` move one option, `Home`/`End` jump to the ends, `Enter`/`Space`
1148
+ pick, `Escape` closes. The highlight is **clamped, not cyclic** — `ArrowUp` at `00:00` stays there,
1149
+ because wrapping to `23:30` reads as a glitch in a list this long — and it is scrolled into view,
1150
+ since the panel is `max-height:15rem` and a highlight nobody can see is no highlight.
1151
+
1152
+ **Deliberate difference between the two paths.** The wrappers keep focus on the **trigger** with
1153
+ `aria-activedescendant` on it, which is what `FdyCombo`, `FdyDatepicker` and `FdyAutocomplete` do;
1154
+ the raw enhancer moves focus **into the panel**. Both are valid APG listbox patterns, and the Vue
1155
+ and React adapters follow their own family rather than the enhancer.
1156
+
1113
1157
  ## Datetime picker — `data-fdy-datetimepicker`
1114
1158
  A **composition** of the two above, not a monolith: nest one datepicker and one timepicker inside
1115
1159
  the wrapper. `freeday-datetime.js` joins them and emits one `fdy-datetime-change` with
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.9.4-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.4)
8
+ [![Release](https://img.shields.io/badge/release-v3.11.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.11.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -44,9 +44,9 @@ enhancer:
44
44
  | **Blazor (net8.0)** | RCL `Freeday.Blazor` | `@bind-Value` |
45
45
  | HTML statis · Svelte · template server-rendered | *(tanpa wrapper)* markup mentah + enhancer, di bawah | event `fdy-*` |
46
46
 
47
- 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
47
+ Ketiga belasnya: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` ·
48
+ `FdyTimepicker` · `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` ·
49
+ `FdyAppShell`. Menulis markup mentah untuk ketiga 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
@@ -256,8 +256,14 @@ terlihat, HTML native dulu sebelum ARIA, komponen interaktif ikut pola WAI-ARIA
256
256
 
257
257
  ## Dukungan browser
258
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.
259
+ 15.4 untuk native `<dialog>`/`::backdrop` dan `accent-color`. `conic-gradient` dan `scroll-snap`
260
+ lebih tua lagi.
261
+
262
+ `test/baseline.test.mjs` membaca tiga angka itu dari baris di atas, mengenumerasi fitur CSS yang
263
+ dipakai kedua stylesheet yang dikirim, dan gagal kalau salah satunya butuh rilis yang tak dijangkau
264
+ floor itu. Sebelum uji itu ada, floor-nya cuma kalimat yang tak bisa ditegakkan siapa pun, dan di
265
+ hari uji ini ditulis sudah ada lima rilis keluar dalam satu hari, empat di antaranya menambal cacat
266
+ Safari yang ketemu di iPhone 7 sesudah terkirim.
261
267
 
262
268
  Dulu floor itu ditentukan `color-mix()` di Safari 16.4, dan sekarang tidak lagi. Harganya memang tak
263
269
  pernah pantas: deklarasi yang mencampur sebuah token membawa `var()`, jadi ia dianggap sah saat
@@ -268,11 +274,33 @@ sekarang ditulis `rgba(var(--color-primary-rgb), .26)`, warnanya sama dan sudah
268
274
  Safari 9; empat aturan yang memang tak bisa dijawab secara statis membawa pasangan `@supports` yang
269
275
  sungguhan. `test/color-mix.test.mjs` menjaga agar yang berikutnya tetap jujur.
270
276
 
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.
277
+ Yang paling baru dipakai kit ini memang melewati floor itu, dengan sengaja, dan tiap satunya
278
+ terdaftar di `test/baseline.test.mjs` beserta apa yang hilang di browser di bawah floor:
279
+
280
+ - `@container` di dialog choose-from-list dipagari `@supports`, dan satu media query lebar viewport
281
+ menumpuk barisnya di bawahnya. Yang tak bisa dilihat cadangan itu adalah dialog sempit di dalam
282
+ viewport lebar, yaitu panel yang didok di desktop, dan di situ dialognya tetap memakai tabel yang
283
+ digulir seperti selama ini.
284
+ - `@starting-style` dan transisi `allow-discrete` menganimasikan modal dan drawer. Di bawah floor
285
+ at-rule-nya dilewati dan shorthand-nya dibuang saat parse, jadi keduanya buka dan tutup tanpa
286
+ animasi, selalu di keadaan akhirnya dan tak pernah separuh jalan.
287
+ - `text-wrap: balance` menyeimbangkan dua kelas judul. Tanpa itu judulnya patah di tempat ia patah
288
+ sebelumnya.
289
+ - `overscroll-behavior: contain` menghentikan gulir di ujung nav dan di dua rail halaman yang
290
+ sticky. Tanpa itu gulirnya lanjut ke halaman di belakangnya, seperti sebelum properti ini ada.
291
+ Tidak ada gantinya di CSS di bawah Safari 16; pilihan lainnya scroll lock di JS, dan shell ini
292
+ sengaja tidak melakukannya.
293
+ - `scrollbar-width: none` menyembunyikan scrollbar carousel, dengan `::-webkit-scrollbar` di
294
+ sebelahnya yang mengerjakan hal sama untuk setiap WebKit di dalam floor.
295
+ - `100dvh` membatasi tinggi rail sticky di layar dari 960px. Safari sudah punya unit itu sejak rilis
296
+ floor-nya sendiri, jadi satu-satunya mesin di dalam floor yang belum punya adalah tiga rilis
297
+ Chrome sebelum unit itu mendarat di sana, dan di situ batasnya hilang dan aside yang panjang
298
+ tumbuh melewati viewport alih-alih digulir di dalam dirinya.
299
+
300
+ Blur backdrop pakai `-webkit-backdrop-filter` + `backdrop-filter`, dan `user-select` membawa kembar
301
+ `-webkit-`-nya dengan cara yang sama. Uji yang sama menegaskan kedua pasangan itu aturan per aturan,
302
+ jadi properti standarnya tak pernah dikirim sendirian. Belum ada CSS build/autoprefixer, jadi
303
+ dukungan di bawah floor itu jadi tanggung jawab konsumen.
276
304
 
277
305
  ## Lisensi
278
306
  [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.9.4-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.9.4)
8
+ [![Release](https://img.shields.io/badge/release-v3.11.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.11.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -44,8 +44,8 @@ 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 twelve: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyAutocomplete` ·
48
- `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`.
47
+ The thirteen: `FdyCombo` · `FdyDatepicker` · `FdyMonthpicker` · `FdyDateRange` · `FdyTimepicker` ·
48
+ `FdyAutocomplete` · `FdyCascade` · `FdyCfl` · `FdyChart` · `FdyTable` · `FdyModal` · `FdyDrawer` · `FdyAppShell`.
49
49
  Hand-writing the raw
50
50
  markup for these inside Vue/React/Blazor *looks* fine at first, because the first render is correct.
51
51
  Then it breaks quietly:
@@ -266,8 +266,14 @@ the "Accessibility" section in `docs/index.html`.
266
266
 
267
267
  ## Browser support
268
268
  **Chrome 105 · Safari 15.4 · Firefox 121.** The floor is set by `:has()`, and by the Safari 15.4
269
- generation of native `<dialog>`/`::backdrop`, `accent-color` and `100dvh`. `conic-gradient` and
270
- `scroll-snap` are older still.
269
+ generation of native `<dialog>`/`::backdrop` and `accent-color`. `conic-gradient` and `scroll-snap`
270
+ are older still.
271
+
272
+ `test/baseline.test.mjs` reads those three numbers out of the line above, enumerates the CSS
273
+ features both shipped stylesheets use, and fails when one of them needs a release the floor does not
274
+ reach. Until that test existed the floor was a sentence nobody could enforce, and the day it was
275
+ written five releases had gone out in one day, four of them fixing Safari defects found on an
276
+ iPhone 7 after shipping.
271
277
 
272
278
  `color-mix()` used to set that floor at Safari 16.4, and it no longer does. It was never worth it:
273
279
  a declaration that mixes a token carries `var()`, so it is assumed valid at parse time and only
@@ -278,11 +284,32 @@ written `rgba(var(--color-primary-rgb), .26)`, which is the same colour and pars
278
284
  the four rules that cannot be answered statically carry a real `@supports` counterpart.
279
285
  `test/color-mix.test.mjs` keeps the next one honest.
280
286
 
281
- The newest things the kit touches (`@container`, `@starting-style`, `text-wrap`) are enhancements: a
282
- browser without them skips the block and keeps the unconditional rule underneath. Backdrop blur uses
283
- `-webkit-backdrop-filter` + `backdrop-filter` (older Safari still works, just without the blur).
284
- There's no CSS build or autoprefixer step, so supporting anything below the floor is the consumer's
285
- responsibility.
287
+ The newest things the kit touches reach past that floor on purpose, and each one is listed in
288
+ `test/baseline.test.mjs` with what a browser below the floor loses:
289
+
290
+ - `@container` in the choose-from-list dialog is fenced with `@supports`, and a viewport-width media
291
+ query stacks the rows underneath it. The case the fallback cannot see is a narrow dialog inside a
292
+ wide viewport, which is a docked panel on a desktop, and there the dialog keeps the scrolling
293
+ table it always had.
294
+ - `@starting-style` and the `allow-discrete` transitions animate the modal and the drawer. Below the
295
+ floor the at-rule is skipped and the shorthand is dropped at parse time, so both open and close
296
+ with no animation, always at their end state and never halfway.
297
+ - `text-wrap: balance` balances two heading classes. Without it a heading wraps where it used to.
298
+ - `overscroll-behavior: contain` stops a scroll at the end of the nav and of the sticky page rails.
299
+ Without it that scroll carries on into the page behind, the way it did before the property
300
+ existed. There is no CSS substitute below Safari 16; the alternative is a scroll lock in JS, which
301
+ this shell deliberately does not do.
302
+ - `scrollbar-width: none` hides the carousel scrollbar, with `::-webkit-scrollbar` beside it doing
303
+ the same job for every WebKit inside the floor.
304
+ - `100dvh` caps the sticky rails on screens from 960px. Safari has had the unit since the floor
305
+ release itself, so the only engines inside the floor without it are the three Chrome releases
306
+ before it landed there, and on those the cap goes away and a long aside grows past the viewport
307
+ instead of scrolling inside itself.
308
+
309
+ Backdrop blur uses `-webkit-backdrop-filter` + `backdrop-filter`, and `user-select` carries its
310
+ `-webkit-` twin the same way. The same test asserts both pairings rule by rule, so the standard
311
+ property never ships alone. There's no CSS build or autoprefixer step, so supporting anything below
312
+ the floor is the consumer's responsibility.
286
313
 
287
314
  ## License
288
315
  [MIT](LICENSE) © 2026 Cahyo D. Kurnianto. Free to use, modify, and distribute, as long as the
@@ -8,9 +8,27 @@
8
8
  <div @ref="Root">
9
9
  <div data-fdy-daterange role="group" aria-label="@AriaLabel">
10
10
  <div data-fdy-datepicker data-role="from"
11
- data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"></div>
11
+ data-value="@From" data-placeholder="@FromPlaceholder" data-min="@Min" data-max="@Max"
12
+ data-fdy-text-prev-month="@PrevMonthLabel"
13
+ data-fdy-text-next-month="@NextMonthLabel"
14
+ data-fdy-text-prev-year="@PrevYearLabel"
15
+ data-fdy-text-next-year="@NextYearLabel"
16
+ data-fdy-text-prev-years="@PrevYearsLabel"
17
+ data-fdy-text-next-years="@NextYearsLabel"
18
+ data-fdy-text-choose-month="@ChooseMonthLabel"
19
+ data-fdy-text-choose-year="@ChooseYearLabel"
20
+ data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
12
21
  <span class="fdy-daterange__sep">@Separator</span>
13
22
  <div data-fdy-datepicker data-role="to"
14
- data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"></div>
23
+ data-value="@To" data-placeholder="@ToPlaceholder" data-min="@Min" data-max="@Max"
24
+ data-fdy-text-prev-month="@PrevMonthLabel"
25
+ data-fdy-text-next-month="@NextMonthLabel"
26
+ data-fdy-text-prev-year="@PrevYearLabel"
27
+ data-fdy-text-next-year="@NextYearLabel"
28
+ data-fdy-text-prev-years="@PrevYearsLabel"
29
+ data-fdy-text-next-years="@NextYearsLabel"
30
+ data-fdy-text-choose-month="@ChooseMonthLabel"
31
+ data-fdy-text-choose-year="@ChooseYearLabel"
32
+ data-fdy-text-back-to-months="@BackToMonthsLabel"></div>
15
33
  </div>
16
34
  </div>
@@ -23,6 +23,26 @@ public partial class FdyDateRange
23
23
  [Parameter] public string? Max { get; set; }
24
24
  [Parameter] public string Separator { get; set; } = "–";
25
25
 
26
+ /* The calendars' own navigation labels, forwarded verbatim to BOTH pickers. Month and weekday
27
+ NAMES follow the page's `lang` through Intl and need nothing here; these are the buttons
28
+ around them. Until now the range was the one picker in the kit whose nav could not be named
29
+ in the page's own language -- FdyDatepicker took these, FdyDateRange dropped them, so an
30
+ Indonesian app got "Previous month" on the only date control it could not reach. Raised
31
+ from keamanan-cluster (#095). */
32
+ [Parameter] public string? PrevMonthLabel { get; set; }
33
+ [Parameter] public string? NextMonthLabel { get; set; }
34
+ [Parameter] public string? PrevYearLabel { get; set; }
35
+ [Parameter] public string? NextYearLabel { get; set; }
36
+ [Parameter] public string? PrevYearsLabel { get; set; }
37
+ [Parameter] public string? NextYearsLabel { get; set; }
38
+ [Parameter] public string? ChooseMonthLabel { get; set; }
39
+ [Parameter] public string? ChooseYearLabel { get; set; }
40
+
41
+ /// <summary>The year grid's title, back to months. Blazor only, because the raw seed both
42
+ /// pickers are built from supports it and <see cref="FdyDatepicker"/> already exposes it;
43
+ /// the Vue and React adapters build no such button.</summary>
44
+ [Parameter] public string? BackToMonthsLabel { get; set; }
45
+
26
46
  private int _token;
27
47
 
28
48
  // initAll(Root) links the daterange (descendant) and inits both child pickers.
@@ -0,0 +1,23 @@
1
+ @namespace Freeday.Blazor
2
+ @inherits FreedayComponentBase<FdyTimepicker>
3
+
4
+ @* Seed-based wrapper over freeday-timepicker.js: the enhancer builds the input-styled trigger
5
+ + time listbox from this empty div and emits fdy-time-select. The enhancer owns that DOM, so
6
+ the seed renders once (ShouldRender => false) and the picked time is read back through
7
+ @bind-Value. Value is "HH:MM" (24-hour) or "".
8
+
9
+ This is what replaces <input type="time">, and not for theming alone: the native picker is
10
+ drawn by the OPERATING SYSTEM, so iOS shows a scroll wheel, Android a clock dial and desktop a
11
+ stepper, each with its own width and value format, and no CSS reaches any of them (#094). *@
12
+ <div @ref="Root" data-fdy-timepicker
13
+ data-value="@Value"
14
+ data-label="@Label"
15
+ data-placeholder="@Placeholder"
16
+ data-step="@StepAttribute"
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)"></div>
@@ -0,0 +1,79 @@
1
+ using Microsoft.AspNetCore.Components;
2
+ using Microsoft.JSInterop;
3
+
4
+ namespace Freeday.Blazor;
5
+
6
+ public partial class FdyTimepicker
7
+ {
8
+ /// <summary>Selected time as <c>HH:MM</c> (24-hour), or "" when unset. Two-way bindable
9
+ /// via <c>@bind-Value</c>.</summary>
10
+ [Parameter] public string? Value { get; set; }
11
+ [Parameter] public EventCallback<string?> ValueChanged { get; set; }
12
+
13
+ /// <summary>Accessible label for the trigger.</summary>
14
+ [Parameter] public string? Label { get; set; }
15
+
16
+ /// <summary>Shown when there is no value. The enhancer's own default is <c>--:--</c>.</summary>
17
+ [Parameter] public string? Placeholder { get; set; }
18
+
19
+ /// <summary>Minutes between options. The enhancer's own default is 30.</summary>
20
+ [Parameter] public int? Step { get; set; }
21
+
22
+ /* null, not "30", for the default: an attribute the enhancer does not need is one no reader
23
+ of the seed has to interpret, and the enhancer already falls back to 30. */
24
+ private string? StepAttribute => Step is null ? null : Step.Value.ToString();
25
+
26
+ /// <summary>Earliest selectable time (<c>HH:MM</c>). Default <c>00:00</c>.</summary>
27
+ [Parameter] public string? Min { get; set; }
28
+
29
+ /// <summary>Latest selectable time (<c>HH:MM</c>). Default <c>23:59</c>.</summary>
30
+ [Parameter] public string? Max { get; set; }
31
+
32
+ /// <summary>Id for the trigger the enhancer builds, so <c>aria-describedby</c> targets and
33
+ /// other references can point at it. Note the trigger is a <c>&lt;button&gt;</c>, which is not
34
+ /// a labelable element, so a <c>&lt;label for&gt;</c> will not activate it — use
35
+ /// <see cref="Label"/>.</summary>
36
+ [Parameter] public string? Id { get; set; }
37
+
38
+ /// <summary>Id of the help or error text the trigger describes itself with.</summary>
39
+ [Parameter] public string? Describedby { get; set; }
40
+
41
+ /// <summary>Greyed and out of the tab order.</summary>
42
+ [Parameter] public bool Disabled { get; set; }
43
+
44
+ /// <summary>Locked/view mode: focusable and showing its time, but the list will not open.
45
+ /// Unlike <see cref="Disabled"/> it keeps tab order and is not greyed.</summary>
46
+ [Parameter] public bool Readonly { get; set; }
47
+
48
+ /// <summary>Marks the field invalid (<c>aria-invalid</c> + the error styling).</summary>
49
+ [Parameter] public bool Invalid { get; set; }
50
+
51
+ // The enhancer owns this control's DOM; init it directly (initAll would skip Root itself).
52
+ protected override ValueTask HydrateAsync() => JS.InvokeVoidAsync("FreedayTimepicker.init", Root);
53
+
54
+ protected override async ValueTask OnHydratedAsync()
55
+ => await SubscribeAsync("fdy-time-select", nameof(OnSelect));
56
+
57
+ // The enhancer owns the built trigger + listbox; never let Blazor re-render the seed.
58
+ protected override bool ShouldRender() => !Hydrated;
59
+
60
+ /* Which is exactly why the three states are pushed rather than re-rendered: the seed above is
61
+ written once, so a page that disables this field later would otherwise be ignored. */
62
+ protected override async Task OnParametersSetAsync()
63
+ {
64
+ if (!Hydrated) return;
65
+ await JS.InvokeVoidAsync("FreedayTimepicker.setState", Root,
66
+ new { disabled = Disabled, @readonly = Readonly, invalid = Invalid });
67
+ }
68
+
69
+ /// <summary>Invoked by the bridge when the reader picks a time (enhancer's fdy-time-select).</summary>
70
+ [JSInvokable]
71
+ public async Task OnSelect(SelectDetail detail)
72
+ {
73
+ Value = detail.Value;
74
+ await ValueChanged.InvokeAsync(detail.Value);
75
+ }
76
+
77
+ /// <summary>The detail shape of the enhancer's <c>fdy-time-select</c> event.</summary>
78
+ public sealed record SelectDetail(string Value);
79
+ }
@@ -29,6 +29,19 @@ export interface FdyDateRangeProps {
29
29
  endPlaceholder?: string;
30
30
  ariaLabel?: string;
31
31
  ariaLabelledby?: string;
32
+ /* Forwarded verbatim to BOTH pickers. Without these the range was the one picker in the kit
33
+ whose calendar nav could not be named in the page's own language: FdyDatepicker takes them,
34
+ FdyDateRange dropped them, so an Indonesian app got "Previous month" on the only date control
35
+ it could not reach. Flat `xxxLabel` props, not a `labels` object, because that is the shape
36
+ every other component in the kit already uses. Raised from keamanan-cluster (#095). */
37
+ prevMonthLabel?: string;
38
+ nextMonthLabel?: string;
39
+ prevYearLabel?: string;
40
+ nextYearLabel?: string;
41
+ prevYearsLabel?: string;
42
+ nextYearsLabel?: string;
43
+ chooseMonthLabel?: string;
44
+ chooseYearLabel?: string;
32
45
  }
33
46
 
34
47
  export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
@@ -57,6 +70,14 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
57
70
  invalid={props.invalid}
58
71
  describedby={props.describedby}
59
72
  placeholder={props.startPlaceholder ?? 'Dari'}
73
+ prevMonthLabel={props.prevMonthLabel}
74
+ nextMonthLabel={props.nextMonthLabel}
75
+ prevYearLabel={props.prevYearLabel}
76
+ nextYearLabel={props.nextYearLabel}
77
+ prevYearsLabel={props.prevYearsLabel}
78
+ nextYearsLabel={props.nextYearsLabel}
79
+ chooseMonthLabel={props.chooseMonthLabel}
80
+ chooseYearLabel={props.chooseYearLabel}
60
81
  />
61
82
  <span className="fdy-daterange__sep" aria-hidden="true">–</span>
62
83
  <FdyDatepicker
@@ -70,6 +91,14 @@ export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
70
91
  invalid={props.invalid}
71
92
  describedby={props.describedby}
72
93
  placeholder={props.endPlaceholder ?? 'Sampai'}
94
+ prevMonthLabel={props.prevMonthLabel}
95
+ nextMonthLabel={props.nextMonthLabel}
96
+ prevYearLabel={props.prevYearLabel}
97
+ nextYearLabel={props.nextYearLabel}
98
+ prevYearsLabel={props.prevYearsLabel}
99
+ nextYearsLabel={props.nextYearsLabel}
100
+ chooseMonthLabel={props.chooseMonthLabel}
101
+ chooseYearLabel={props.chooseYearLabel}
73
102
  />
74
103
  </div>
75
104
  );