@cahyo-dimas/freeday 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/COMPONENTS.md +251 -8
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/adapters/blazor/FdyDateRange.razor.cs +10 -0
- package/adapters/blazor/FdyTable.razor +6 -2
- package/adapters/blazor/FdyTable.razor.cs +21 -0
- package/adapters/blazor/TableTypes.cs +4 -0
- package/adapters/react/components/FdyDateRange.tsx +8 -2
- package/adapters/react/components/FdyTable.tsx +24 -3
- package/adapters/react/index.d.ts +1 -1
- package/adapters/vue/components/FdyDateRange.vue +11 -5
- package/adapters/vue/components/FdyTable.vue +18 -2
- package/dist/freeday-confirm.js +101 -0
- package/dist/freeday-copy.js +58 -0
- package/dist/freeday-datepicker.js +17 -2
- package/dist/freeday-form.js +30 -1
- package/dist/freeday-search.js +77 -0
- package/dist/freeday.bundle.css +83 -6
- package/dist/freeday.css +83 -6
- package/dist/freeday.d.ts +17 -2
- package/dist/freeday.js +286 -3
- package/docs/agent-onboarding.md +8 -0
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/components/avatar.css +2 -0
- package/src/components/card.css +7 -0
- package/src/components/composition.css +36 -0
- package/src/components/description-list.css +11 -1
- package/src/components/selection.css +6 -0
- package/src/components/states.css +7 -0
- package/src/components/table.css +14 -5
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,90 @@ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)`
|
|
|
9
9
|
lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
|
|
10
10
|
sisi kit.
|
|
11
11
|
|
|
12
|
+
## [3.6.0] - 2026-10-22
|
|
13
|
+
|
|
14
|
+
**Tiga belas hal yang dibangun ulang sebuah app back-office karena kit belum punya.** App-nya tidak
|
|
15
|
+
memakai Freeday (Vue + PrimeVue), jadi yang diangkat adalah polanya, bukan komponennya: setiap
|
|
16
|
+
kandidat dibandingkan dengan kit, dan hanya yang generik dan benar-benar belum ada yang masuk
|
|
17
|
+
(`#059`–`#071`). Satu di antaranya cacat yang kit punya juga: submit yang ditolak memfokus field
|
|
18
|
+
yang tak terlihat. Satu lagi mengubah tampilan setiap tabel, jadi dikirim sebagai opt-in dan
|
|
19
|
+
dijadwalkan jadi default di 4.0 (NEXT-UP #16). Tidak ada tipe publik yang menyempit dan tidak ada
|
|
20
|
+
tampilan yang berubah tanpa diminta, jadi ini rilis minor.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`Freeday.confirm()`** (`#060`). Pertanyaan ya/tidak sebelum aksi yang tak bisa diulang, tanpa
|
|
24
|
+
merakit dialog sendiri. Mengembalikan `Promise<boolean>`: `true` saat accept, `false` saat reject,
|
|
25
|
+
Escape atau klik backdrop. `details[]` tampil sebagai daftar di bawah pesan, untuk konfirmasi yang
|
|
26
|
+
harus menyebut temuannya. Fokus awal di tombol reject, teks dipasang sebagai teks (bukan HTML), dan
|
|
27
|
+
kedua tombol punya hook `data-fdy-confirm-accept` / `-reject` untuk test. Tipe ada di
|
|
28
|
+
`freeday.d.ts`; Blazor memanggilnya lewat `JS.InvokeAsync<bool>`.
|
|
29
|
+
- **`[data-fdy-search]`** (`#061`, menjawab NEXT-UP #10). `.fdy-input-group` yang mencari saat
|
|
30
|
+
pengetik berhenti sejenak (`data-fdy-search-delay`, default 400 ms), langsung saat Enter, dan
|
|
31
|
+
punya tombol clear. Ketiga jalur berakhir di satu event `fdy-search {term}`. Clear menulis lewat
|
|
32
|
+
setter native dan memicu `input` native, jadi `v-model`, `onChange` React dan `@bind` Blazor ikut.
|
|
33
|
+
- **`[data-fdy-copy]`** (`#062`). Tombol yang menyalin nilainya sendiri lalu memberi toast. Tanpa
|
|
34
|
+
Clipboard API (http) pembaca diberi tahu untuk menyalin manual. Listener-nya didelegasikan di
|
|
35
|
+
`document`, jadi tombol yang dirender belakangan tak perlu hidrasi.
|
|
36
|
+
- Ketiganya dijaga `browser/confirm-search-copy.mjs` (7 spec), dan tiap spec diverifikasi dengan
|
|
37
|
+
mutasi: delapan mutasi pada kode baru, delapan merah.
|
|
38
|
+
- **`settled` pada date range** (`#063`). Vue `@settled`, React `onSettled`, Blazor `Settled`,
|
|
39
|
+
jalur mentah `fdy-daterange-settled`. Terpancar hanya saat kedua ujung terisi atau keduanya
|
|
40
|
+
kosong. Sebelumnya setiap stack memancarkan per ujung, jadi list yang reload saat berubah memuat
|
|
41
|
+
data dengan tanggal awal saja lalu memuat lagi saat tanggal akhir dipilih. `change` dan `v-model`
|
|
42
|
+
tidak berubah. Dijaga `browser/daterange-settled.mjs` (4 spec) + `FdyDateRangeTests` (2 bUnit),
|
|
43
|
+
lima mutasi, lima merah.
|
|
44
|
+
- **`.fdy-dl--grid`** (`#066`). Grid fakta: label di atas nilai, kolom sebanyak yang muat di lebar
|
|
45
|
+
list itu SENDIRI, maksimal tiga. Diukur dari elemen, bukan jendela, jadi markup yang sama tiga
|
|
46
|
+
kolom di area utama dan satu kolom di rail sempit tanpa modifier. Semua `.fdy-dl dd` kini
|
|
47
|
+
`overflow-wrap:anywhere`, jadi email atau nomor panjang tidak lagi tercetak di atas kolom sebelah.
|
|
48
|
+
- **`.fdy-card__head` + `__action`** (`#070`). Header kartu: tanda, judul yang di-truncate, dan
|
|
49
|
+
satu aksi di kanan, dengan garis selebar kartu. Tandanya `.fdy-avatar --sm --square --tone-N`.
|
|
50
|
+
- **`.fdy-avatar--square`** untuk tanda yang bukan orang (logo perusahaan, chip ikon kartu).
|
|
51
|
+
- **Hint state switch** (`#068`). Dua baris `[data-fdy-when="on|off"]` di satu `.fdy-help`; kit
|
|
52
|
+
menampilkan yang cocok dengan state sekarang, tanpa JS. `aria-describedby` menunjuk ke
|
|
53
|
+
pembungkusnya, jadi pembaca layar hanya mendengar baris yang tampil (diverifikasi lewat deskripsi
|
|
54
|
+
aksesibel yang dihitung Chrome).
|
|
55
|
+
- **`.fdy-state--compact`** (`#069`) untuk state di dalam kartu kecil.
|
|
56
|
+
- **Empty state `FdyTable` tahu kenapa ia kosong** (`#069`). Slot Vue `empty` dan fungsi React
|
|
57
|
+
`empty` menerima `{ filtered, clearFilters }`; Blazor punya `EmptyTemplate`
|
|
58
|
+
(`RenderFragment<FdyTableEmptyContext>`). "Tidak ada yang cocok" bisa menawarkan Clear, "belum
|
|
59
|
+
ada data" menawarkan Create.
|
|
60
|
+
- **`.fdy-page__columns`** (`#065`, menutup "Form master-detail / 2 kolom" yang ditunda di
|
|
61
|
+
NEXT-UP). Rail `__lead` / `__aside` di samping `__main`; lebar dipilih dari rail mana yang ada
|
|
62
|
+
(8/4, 3/9, 3/6/3). Saat menumpuk, rail ringkasan didahulukan. Mulai `md` rail sticky dan
|
|
63
|
+
scroll sendiri, jadi total dan status tetap terlihat dan rail yang lebih tinggi dari layar tidak
|
|
64
|
+
menggantung. Diadaptasi dari layout yang di app asalnya menahan seluruh halaman; di kit shell-nya
|
|
65
|
+
tetap scroll halaman, jadi tidak ada consumer yang dipaksa mengubah layout-nya. Dijaga
|
|
66
|
+
`browser/page-columns.mjs` (5 spec, 6 mutasi, 6 merah).
|
|
67
|
+
- **`.fdy-table--hover-actionable`** (`#064`), prop `hoverActionable` / `HoverActionable` di ketiga
|
|
68
|
+
adapter. Hanya baris yang melakukan sesuatu (`rowActivatable`) yang ber-hover (baris terpilih tetap berwarna terpilih);
|
|
69
|
+
baris inert kembali ke warna istirahatnya sendiri, termasuk stripe, dan sel beku tetap opak.
|
|
70
|
+
Opt-in di 3.x supaya tidak ada tabel yang berubah; direncanakan jadi default di 4.0. Hover dasar
|
|
71
|
+
kini membaca `--_fdy-row-hover` (privat), tampilannya tidak berubah. Dijaga
|
|
72
|
+
`browser/table-hover.mjs` (4 spec, 5 mutasi, 5 merah) + `FdyTableHoverTests` (2 bUnit).
|
|
73
|
+
- **Docs: breadcrumb** (`#071 §8`). `aria-current="page"` hanya untuk crumb yang memang halaman ini;
|
|
74
|
+
trail yang berhenti di induknya (form record yang crumb terakhirnya list) membiarkan crumb itu
|
|
75
|
+
tetap link.
|
|
76
|
+
- **Docs: Record actions** (`#067`). Urutan tetap untuk aksi form di `.fdy-page__actions`:
|
|
77
|
+
Find | New, maksimal dua aksi inline, sisanya di menu More dengan urutan deklarasi yang sama dan
|
|
78
|
+
warna danger yang tetap terbawa, Save terakhir dan satu-satunya tombol solid.
|
|
79
|
+
- Dijaga `browser/display-parts.mjs` (4), `browser/table-empty.mjs` (2) dan `FdyTableEmptyTests`
|
|
80
|
+
(2 bUnit). Setiap perilaku baru diverifikasi dengan mutasi (13 mutasi, 13 merah).
|
|
81
|
+
### Fixed
|
|
82
|
+
- **Date range jalur mentah: ujung yang dihapus kini diumumkan.** Tanggal awal yang melewati
|
|
83
|
+
tanggal akhir mengosongkan tanggal akhir tanpa event apa pun, jadi `To` di Blazor (yang hanya
|
|
84
|
+
mendengar pilihan) tetap memegang tanggal yang sudah tidak ada. Pengosongan itu sekarang
|
|
85
|
+
memancarkan `fdy-datepicker-change` dengan `value: ''`.
|
|
86
|
+
- **Submit yang ditolak kini memperlihatkan field yang ditolaknya** (`#059`). `freeday-form.js`
|
|
87
|
+
memanggil `focus()` pada kontrol invalid pertama dan berhenti di situ. Kalau kontrol itu tidak
|
|
88
|
+
dirender (di dalam `<details>` tertutup, di tab yang tak dipilih, di panel stepper yang sudah
|
|
89
|
+
dilewati), `focus()` tidak melakukan apa pun dan tidak melapor apa pun: field memerah di tempat
|
|
90
|
+
yang tak terlihat, dan tombol Save terbaca mati. Sekarang penghalangnya dibuka dulu lewat kontrol
|
|
91
|
+
milik komponennya sendiri, supaya state komponen itu tetap cocok dengan yang tampil: `<details>`
|
|
92
|
+
di-`open`, tab-nya diklik, tombol step-nya diklik. Step yang **belum dicapai** sengaja tidak
|
|
93
|
+
dilompati, karena itu sama dengan melewati guard `fdy-step-before-change`. Dijaga
|
|
94
|
+
`browser/form-reveal.mjs` (5 spec); empat di antaranya merah sebelum perbaikan.
|
|
95
|
+
|
|
12
96
|
## [3.5.0] - 2026-09-15
|
|
13
97
|
|
|
14
98
|
**Sebuah angka yang hanya bisa ditemukan dengan membaca stylesheet kit.** `#058` (dari
|
package/COMPONENTS.md
CHANGED
|
@@ -77,7 +77,7 @@ element is fine. Events are bubbling
|
|
|
77
77
|
| `data-fdy-autocomplete` | `freeday-autocomplete.js` | `FreedayAutocomplete` | `fdy-autocomplete-select` |
|
|
78
78
|
| `data-fdy-cascade` | `freeday-cascade.js` | `FreedayCascade` | `fdy-cascade-change` |
|
|
79
79
|
| `data-fdy-cfl` | `freeday-cfl.js` | `FreedayCfl` | `fdy-cfl-select` |
|
|
80
|
-
| `data-fdy-datepicker`, `data-fdy-daterange` | `freeday-datepicker.js` | `FreedayDatepicker` | `fdy-datepicker-change` |
|
|
80
|
+
| `data-fdy-datepicker`, `data-fdy-daterange` | `freeday-datepicker.js` | `FreedayDatepicker` | `fdy-datepicker-change`, `fdy-daterange-settled` |
|
|
81
81
|
| `data-fdy-timepicker` | `freeday-timepicker.js` | `FreedayTimepicker` | `fdy-time-select` |
|
|
82
82
|
| `data-fdy-datetimepicker` | `freeday-datetime.js` | `FreedayDatetime` | `fdy-datetime-change` |
|
|
83
83
|
| `data-fdy-table` | `freeday-table.js` | `FreedayTable` | `fdy-table-change` |
|
|
@@ -93,9 +93,12 @@ element is fine. Events are bubbling
|
|
|
93
93
|
| `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
|
|
94
94
|
| `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
|
|
95
95
|
| `data-fdy-number` | `freeday-number.js` | `FreedayNumber` | — (native `input`, `change`) |
|
|
96
|
+
| `data-fdy-search` | `freeday-search.js` | `FreedaySearch` | `fdy-search` (+ native `input` on clear) |
|
|
97
|
+
| `data-fdy-copy="value"` (delegated, no hydration) | `freeday-copy.js` | `FreedayCopy` | `fdy-copy` |
|
|
96
98
|
| `data-fdy-drawer="id"` | `freeday-drawer.js` | `FreedayDrawer` | — |
|
|
97
99
|
| `data-fdy-chart="type"` | `freeday-chart.js` | `FreedayChart` | — (`.update(el)` to repaint) |
|
|
98
100
|
| — (imperative) | `freeday-toast.js` | `Freeday.toast()` / `Freeday.dismiss()` | — |
|
|
101
|
+
| — (imperative, returns `Promise<boolean>`) | `freeday-confirm.js` | `Freeday.confirm()` | — |
|
|
99
102
|
| — (stamps `<html data-breakpoint>`) | `freeday-breakpoint.js` | `FreedayBreakpoint` | `fdy-breakpoint-change` |
|
|
100
103
|
| — (internal positioning) | `freeday-popover.js` | `FreedayPopover` | — |
|
|
101
104
|
|
|
@@ -289,6 +292,78 @@ carry the doctrine in markup.
|
|
|
289
292
|
</div>
|
|
290
293
|
```
|
|
291
294
|
|
|
295
|
+
### Document columns — `.fdy-page__columns`
|
|
296
|
+
|
|
297
|
+
A record or form page with side rails: `__lead` on the left for the record's identity (avatar,
|
|
298
|
+
number, status), `__main` for the sections, `__aside` on the right for the summary (totals, what
|
|
299
|
+
happens next). The column widths follow which rails are **present**, so no screen picks them:
|
|
300
|
+
|
|
301
|
+
| Children | From `md` | From `lg` |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| `__main` | one column | one column |
|
|
304
|
+
| `__main` + `__aside` | 8 / 4 | 8 / 4 |
|
|
305
|
+
| `__lead` + `__main` | 3 / 9 | 3 / 9 |
|
|
306
|
+
| `__lead` + `__main` + `__aside` | rails stacked on the left, main on the right | 3 / 6 / 3 |
|
|
307
|
+
|
|
308
|
+
- Below `md` everything stacks, and **the summary rail comes first**. Last, it would sit under
|
|
309
|
+
every section, and the button in it with it.
|
|
310
|
+
- From `md` each rail is **sticky and scrolls on its own**: the total and the status stay in view
|
|
311
|
+
while `__main` scrolls with the page, and a rail taller than the screen scrolls inside itself
|
|
312
|
+
instead of hanging its end off the bottom. The exception is both rails sharing the left column
|
|
313
|
+
(between `md` and `lg`): two sticky rails in one column would slide over each other, so there they
|
|
314
|
+
scroll with the page.
|
|
315
|
+
- Inside `.fdy-app` the rails stick below the topbar. Set `--fdy-page-sticky-top` on the columns or
|
|
316
|
+
an ancestor if your header is taller.
|
|
317
|
+
- A rail's box starts `--space-1` early on every side, room for a focus ring that a scrolling box
|
|
318
|
+
would otherwise clip. Its content still lines up with `__main`.
|
|
319
|
+
- Put `.fdy-dl--grid` in a rail as is: it measures the rail, not the window, so it stays one column.
|
|
320
|
+
|
|
321
|
+
```html
|
|
322
|
+
<div class="fdy-page__columns">
|
|
323
|
+
<aside class="fdy-page__lead" aria-label="Record">…</aside>
|
|
324
|
+
<div class="fdy-page__main">…sections…</div>
|
|
325
|
+
<aside class="fdy-page__aside" aria-label="Summary">…</aside>
|
|
326
|
+
</div>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Record actions — a form's `.fdy-page__actions`
|
|
330
|
+
|
|
331
|
+
A document or master form carries more than one button: its own actions (Print, Send, Cancel
|
|
332
|
+
document, Copy from…) and Save. Without a rule the row grows with every module, reflows at tablet
|
|
333
|
+
width and pushes Save off the end, and two screens with the same actions order them differently.
|
|
334
|
+
The kit's rule, in this fixed order:
|
|
335
|
+
|
|
336
|
+
1. **Find | New** as one `.fdy-btn-group`, ghost. Hide New on the create screen itself: a button
|
|
337
|
+
that navigates to the screen you are on appears to do nothing.
|
|
338
|
+
2. **At most two** of the form's own actions inline, ghost, in the order they are declared.
|
|
339
|
+
3. **More**: an `.fdy-menu` holding every other action, in the **same declared order**. A
|
|
340
|
+
destructive item keeps `.fdy-menu__item--danger`: an action that loses its colour on the way
|
|
341
|
+
into a menu is one the reader no longer recognises. Keep each action's test hook the same
|
|
342
|
+
whether it is inline or in the menu.
|
|
343
|
+
4. **Save**, last, the **only** solid button on the row. Never green: green says something already
|
|
344
|
+
succeeded.
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<div class="fdy-page__actions">
|
|
348
|
+
<div class="fdy-btn-group" role="group" aria-label="Records">
|
|
349
|
+
<a class="fdy-btn fdy-btn--ghost" href="/orders">Find</a>
|
|
350
|
+
<a class="fdy-btn fdy-btn--ghost" href="/orders/new">New</a>
|
|
351
|
+
</div>
|
|
352
|
+
<button class="fdy-btn fdy-btn--ghost" type="button">Print</button>
|
|
353
|
+
<button class="fdy-btn fdy-btn--ghost" type="button">Send</button>
|
|
354
|
+
<div class="fdy-menu-wrap" data-fdy-menu>
|
|
355
|
+
<button class="fdy-btn fdy-btn--ghost" type="button" aria-haspopup="menu" aria-expanded="false">More</button>
|
|
356
|
+
<ul class="fdy-menu fdy-menu--end" role="menu" hidden>
|
|
357
|
+
<li role="none"><button class="fdy-menu__item" role="menuitem">Copy from quotation</button></li>
|
|
358
|
+
<li role="none"><button class="fdy-menu__item fdy-menu__item--danger" role="menuitem">Cancel document</button></li>
|
|
359
|
+
</ul>
|
|
360
|
+
</div>
|
|
361
|
+
<button class="fdy-btn" type="submit" form="order-form">Save</button>
|
|
362
|
+
</div>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Ask before a destructive action with `Freeday.confirm({ tone: 'danger', … })`.
|
|
366
|
+
|
|
292
367
|
## Type roles & text
|
|
293
368
|
One role per level of hierarchy. Never re-use a card title for a page title.
|
|
294
369
|
|
|
@@ -513,7 +588,7 @@ cannot guarantee.
|
|
|
513
588
|
|
|
514
589
|
| Type | What the UA still draws | What to do |
|
|
515
590
|
|---|---|---|
|
|
516
|
-
| `search` | WebKit's clear (×) button, in OS colours | **Left alone on purpose** — it is the only way to empty the field. Want it themed
|
|
591
|
+
| `search` | WebKit's clear (×) button, in OS colours | **Left alone on purpose** — it is the only way to empty the field. Want it themed, and want clearing to search again? Use [`data-fdy-search`](#search-box--data-fdy-search), which is `type="text"` with a clear button of its own. |
|
|
517
592
|
| `date` `time` `datetime-local` | the picker indicator glyph, and the whole native picker | Use `.fdy-datepicker` / `.fdy-timepicker` / `.fdy-datetimepicker` instead — the kit ships its own. |
|
|
518
593
|
|
|
519
594
|
`number` used to be a third: the UA spin buttons are unthemeable, so `.fdy-input[type="number"]`
|
|
@@ -557,6 +632,31 @@ the shared border, focus ring and error promotion. Needs `freeday-number.js`.
|
|
|
557
632
|
`aria-label`, so pointer and browse-mode users still get a named control.
|
|
558
633
|
- `type="button"` is required. Inside a `<form>`, a bare `<button>` submits it.
|
|
559
634
|
|
|
635
|
+
## Search box — `[data-fdy-search]`
|
|
636
|
+
The search box above a list: it searches when the reader pauses typing, at once on Enter, and has a
|
|
637
|
+
clear button that brings every row back. **Not a new block.** It is an `.fdy-input-group`, like the
|
|
638
|
+
number field. Needs `freeday-search.js`.
|
|
639
|
+
|
|
640
|
+
```html
|
|
641
|
+
<div class="fdy-input-group" data-fdy-search>
|
|
642
|
+
<span class="fdy-input-group__addon fdy-input-group__addon--icon"><!-- search svg --></span>
|
|
643
|
+
<input class="fdy-input" type="text" aria-label="Search orders">
|
|
644
|
+
<button type="button" class="fdy-input-group__btn" data-fdy-search-clear aria-label="Clear search">×</button>
|
|
645
|
+
</div>
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- **One event for all three paths.** The group emits bubbling `fdy-search` with `detail.term`
|
|
649
|
+
(trimmed) after the pause, on Enter, and on clear. `""` means "show everything again". Listen to
|
|
650
|
+
that and nothing else.
|
|
651
|
+
- The pause is `data-fdy-search-delay` in ms on the group, default `400`. A new keystroke restarts
|
|
652
|
+
it; Enter and clear send at once and cancel what was pending.
|
|
653
|
+
- `type="text"`, not `search`: WebKit's own × is unthemed, and emptying a field with it never
|
|
654
|
+
searched again on a list that only listened for Enter.
|
|
655
|
+
- The clear button is yours, and so is its `aria-label`. The enhancer hides it while the box is
|
|
656
|
+
empty. Clearing writes through the native value setter and fires a native `input`, so `v-model`,
|
|
657
|
+
a React `onChange` and Blazor `@bind` see the empty value, then puts focus back in the box.
|
|
658
|
+
- Enter is `preventDefault`ed, so a search box inside a `<form>` does not submit it.
|
|
659
|
+
|
|
560
660
|
## Checkbox · radio · switch
|
|
561
661
|
Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrapping `<label>`;
|
|
562
662
|
`.fdy-checkbox` on a bare `<input type="checkbox">` used inside tables/trees.
|
|
@@ -566,6 +666,23 @@ Native inputs, styled. `.fdy-check` · `.fdy-radio` · `.fdy-switch` on the wrap
|
|
|
566
666
|
<label class="fdy-switch"><input type="checkbox"> Enable notifications</label>
|
|
567
667
|
```
|
|
568
668
|
|
|
669
|
+
**A state hint** says what the switch's **current** state means, for a label that does not explain
|
|
670
|
+
"on" by itself ("Approval" on a notification rule). Two lines in one `.fdy-help`, and the kit
|
|
671
|
+
shows the one that matches. Point `aria-describedby` at the **wrapper**, not at the two lines: a
|
|
672
|
+
hidden child of a referenced element is skipped, so a screen reader hears only the line on screen,
|
|
673
|
+
while each line referenced directly would be read even when hidden. Zero JS; works for
|
|
674
|
+
`.fdy-check` too. Needs a `.fdy-field` around both.
|
|
675
|
+
|
|
676
|
+
```html
|
|
677
|
+
<div class="fdy-field">
|
|
678
|
+
<label class="fdy-switch"><input type="checkbox" aria-describedby="appr-hint"> Approval</label>
|
|
679
|
+
<div class="fdy-help" id="appr-hint">
|
|
680
|
+
<span data-fdy-when="on">Each change waits for a supervisor.</span>
|
|
681
|
+
<span data-fdy-when="off">Changes apply at once; nobody reviews them.</span>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
```
|
|
685
|
+
|
|
569
686
|
## Select / combobox — `.fdy-combo`
|
|
570
687
|
> **Typed wrapper: `<FdyCombo>`**. Vue (`v-model`) · React (`value`/`onChange`) · Blazor (`@bind-Value`). In those stacks use the wrapper; the markup below is for stacks without an adapter (and is what the wrapper renders).
|
|
571
688
|
|
|
@@ -857,7 +974,8 @@ page's `<html lang>`), `clearable` and `clearLabel` (this path builds no clear b
|
|
|
857
974
|
| Prop | Type | What it does |
|
|
858
975
|
|---|---|---|
|
|
859
976
|
| `modelValue` · `value` | `DateRangeValue` = `{ start, end }`, each `string \| null` | Both ends at once, so the pair can never be half-applied. |
|
|
860
|
-
| `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. |
|
|
977
|
+
| `onChange` | `(value: DateRangeValue) => void` | React. Vue emits `update:modelValue` and `change`. Fires on **each** end, which is right for a binding and wrong for a reload. |
|
|
978
|
+
| `onSettled?` | `(value: DateRangeValue) => void` | React. Vue emits `settled`, Blazor `Settled` (`SettledRange(From, To)`), the raw path `fdy-daterange-settled` `{ from, to }` on the group. Fires only when **both ends are chosen or both are empty**, so a list that reloads on it never filters on half a range. |
|
|
861
979
|
| `min?` · `max?` | `string` | ISO bounds for both ends. The end can never precede the start. |
|
|
862
980
|
| `locale?` | `string` | As on the single picker. |
|
|
863
981
|
| `startPlaceholder?` · `endPlaceholder?` | `string` | One per field. |
|
|
@@ -874,7 +992,9 @@ The enhancer builds everything.
|
|
|
874
992
|
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
|
|
875
993
|
`__dow` `__day` `__month` `__year`
|
|
876
994
|
- Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
|
|
877
|
-
get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start
|
|
995
|
+
get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start:
|
|
996
|
+
a start picked past the end clears the end, and that clear fires `fdy-datepicker-change` with
|
|
997
|
+
`value: ''` like a pick. Reload on `fdy-daterange-settled`, not on each change.
|
|
878
998
|
**Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor). Use it in those stacks.
|
|
879
999
|
**Getting to a distant date.** The title is a **button**, and it drills up one level each press:
|
|
880
1000
|
day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
|
|
@@ -982,6 +1102,13 @@ blur/input. Needs `freeday-form.js`.
|
|
|
982
1102
|
- Per-control messages: `data-fdy-msg-required`, `-type`, `-minlength`, `-min`, `-mismatch`, …
|
|
983
1103
|
- Cross-field: `data-fdy-match="#otherFieldId"`
|
|
984
1104
|
- Emits `fdy-form-valid` / `fdy-form-invalid`.
|
|
1105
|
+
- **A refused field is revealed before it is focused**, because `focus()` on a control that is not
|
|
1106
|
+
rendered does nothing. A closed `<details>` around it (accordion, tree, nav group) is opened, an
|
|
1107
|
+
unselected tab is selected by clicking it, and a `.fdy-step-panel` left behind is returned to by
|
|
1108
|
+
clicking its step. A step **not yet reached** is never jumped to, since that would skip its
|
|
1109
|
+
`fdy-step-before-change` guard: put the submit where every refusable field is already reachable,
|
|
1110
|
+
or mark the step `is-error` from `fdy-form-invalid`. Anything else hiding the field (your own
|
|
1111
|
+
`hidden`, a closed drawer) stays as you left it.
|
|
985
1112
|
|
|
986
1113
|
## Password & mask
|
|
987
1114
|
`data-fdy-password` on `<input type="password">` adds a reveal toggle (reusing the input-group
|
|
@@ -1080,6 +1207,15 @@ hover and selection are ordered so the later state always wins. Override the col
|
|
|
1080
1207
|
Tier-3 token **`--fdy-table-stripe`** (set it on any ancestor; the rule reads it as a `var()`
|
|
1081
1208
|
fallback, so your value wins without a specificity fight). Frozen columns stay striped.
|
|
1082
1209
|
|
|
1210
|
+
**`--hover-actionable`** keeps the hover tint for rows that do something
|
|
1211
|
+
(`.fdy-table__row--activatable`) and gives every other row none. A selected row keeps its selected
|
|
1212
|
+
colour either way. A tinted row
|
|
1213
|
+
with no click handler promises a target that is not there: people click it, nothing happens, and
|
|
1214
|
+
the button they needed was elsewhere in the row. Striping is what keeps the eye on a line; hover
|
|
1215
|
+
says "target". An inert row hovers back to its own resting colour, stripe included, and a frozen
|
|
1216
|
+
cell to its opaque surface. Opt-in in 3.x so no existing table changes; planned as the default
|
|
1217
|
+
for 4.0.
|
|
1218
|
+
|
|
1083
1219
|
```html
|
|
1084
1220
|
<div class="fdy-table-wrap">
|
|
1085
1221
|
<table class="fdy-table">
|
|
@@ -1187,12 +1323,13 @@ provide the state prop for a concern and you own that concern instead. React nam
|
|
|
1187
1323
|
| `pageSizes?` · `onPageSizeChange?` | `readonly number[]` · `(size) => void` | Offer a rows-per-page control. Server mode reports the pick through the page event (read its `size`); client mode applies it itself and reports it too, so it works with nothing wired. |
|
|
1188
1324
|
| `pager?` | `boolean` | Default true. Turn it off to withhold the table's own footer when the screen renders one. |
|
|
1189
1325
|
| `loading?` | `boolean` | Show the in-table loading state. |
|
|
1190
|
-
| `emptyText?` · `empty?` | `string` · `ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). |
|
|
1326
|
+
| `emptyText?` · `empty?` | `string` · `ReactNode \| (ctx) => ReactNode` | The empty state as text, or as markup (React `empty`, Vue's `empty` slot). As a React function or through the Vue slot's props it receives `{ filtered, clearFilters }`: whether an active column filter is what emptied the table, and how to clear every filter (through `onFiltersChange` / `update:filters` when they are controlled). "No rows match" wants a Clear button, "nothing here yet" a Create one. |
|
|
1191
1327
|
| `ariaLabel?` | `string` | Names the table for assistive tech. |
|
|
1192
1328
|
| `rowActivatable?` · `onRowActivate?` | `boolean` · `(row) => void` | Rows become focusable and activate on click, Enter or Space. |
|
|
1193
1329
|
| `rowClass?` | `(row: Row) => string \| undefined` | Per-row class hook, e.g. marking the selected row. |
|
|
1194
1330
|
| `expandedKeys?` · `renderRowDetail?` | `ReadonlyArray<string \| number>` · `(row) => ReactNode` | Controlled expansion: these keys get a full-width detail row. Vue uses the `row-detail` slot. |
|
|
1195
1331
|
| `striped?` | `boolean` | Zebra-stripe the body rows (`.fdy-table--striped`). |
|
|
1332
|
+
| `hoverActionable?` | `boolean` | Hover only the rows that do something (`rowActivatable`), `.fdy-table--hover-actionable`. A tint on a row with no handler promises a target that is not there. Inert rows keep their resting colour, stripe included. Off by default so existing tables do not change; planned as the default for 4.0. |
|
|
1196
1333
|
| `selectable?` | `boolean` | Render the checkbox column and the bulk bar. |
|
|
1197
1334
|
| `selectedKeys?` · `onSelectedKeysChange?` | `ReadonlyArray<string \| number>` · `(keys) => void` | Controlled selection, as `rowKey` values — the same identity `expandedKeys` uses, because a key survives the re-fetch that replaces every row object. Omit `selectedKeys` and the column still works, keeping the selection internally. The callback fires in **both** modes, so a screen can watch the selection without owning it. Vue emits `update:selectedKeys`; Blazor binds `@bind-SelectedKeys`. |
|
|
1198
1335
|
| `bulkActions?` | `ReactNode` | React. Content in the bulk bar beside the count; Vue uses the `bulk-actions` slot (scoped: `keys`, `clear`), Blazor `BulkContent`. |
|
|
@@ -1204,7 +1341,8 @@ provide the state prop for a concern and you own that concern instead. React nam
|
|
|
1204
1341
|
|
|
1205
1342
|
Blazor matches this surface (`LoadingText` and `EmptyContent` in place of `emptyText`/`empty`,
|
|
1206
1343
|
`Toolbar` and `RowDetail` as `RenderFragment`s) and is the one adapter that also exposes
|
|
1207
|
-
`FiltersChanged`.
|
|
1344
|
+
`FiltersChanged`. Its filter-aware empty state is `EmptyTemplate`, a
|
|
1345
|
+
`RenderFragment<FdyTableEmptyContext>` (`Filtered`, `ClearFilters`) that wins over `EmptyContent`.
|
|
1208
1346
|
|
|
1209
1347
|
**Select-all ticks the current page, not the whole result set.** A header checkbox that silently
|
|
1210
1348
|
selects rows the reader cannot see is how a bulk delete goes wrong, so it acts on the rows on
|
|
@@ -1332,6 +1470,11 @@ Empty / error placeholder for a data area. `--danger` for failures. Parts: `__ic
|
|
|
1332
1470
|
(`aria-hidden="true"`) `__title` `__text` `__actions`. Put it inside the card or table area it
|
|
1333
1471
|
replaces.
|
|
1334
1472
|
|
|
1473
|
+
`--compact` is the same state inside a small card or dashboard widget: icon and text on one line,
|
|
1474
|
+
`__actions` pushed to the end, no `__title`. Give a failed load its own `--compact --danger` line
|
|
1475
|
+
rather than rendering zero: a widget that shows 0 for a request that failed reads as "nothing to
|
|
1476
|
+
do", the opposite of what happened.
|
|
1477
|
+
|
|
1335
1478
|
## Charts — `data-fdy-chart`
|
|
1336
1479
|
> **Typed wrapper: `<FdyChart>`**. data props in all three (`type` + `values` / `series`); it repaints on data change, so `FreedayChart.update(el)` is only for the raw path below.
|
|
1337
1480
|
|
|
@@ -1433,12 +1576,55 @@ responsive `.fdy-datatable` should become below `md`, rather than a stack of `.f
|
|
|
1433
1576
|
|
|
1434
1577
|
## Description list — `.fdy-dl`
|
|
1435
1578
|
Key–value detail view: `<dl class="fdy-dl">` with `<dt>`/`<dd>` pairs. `--rows` for a row-per-pair
|
|
1436
|
-
layout.
|
|
1579
|
+
layout. Every `dd` wraps a long unbreakable value (an email, an order code, a 13-digit phone) inside
|
|
1580
|
+
its column instead of printing over the next one.
|
|
1581
|
+
|
|
1582
|
+
**`--grid` is the facts panel:** caption above value, each pair wrapped in a `<div>` (valid inside
|
|
1583
|
+
`<dl>`), in as many columns as the list's **own** width holds, never more than three. It is sized
|
|
1584
|
+
by the element, not the window, so the same markup is three columns in a wide main column and one
|
|
1585
|
+
column in a narrow side rail, with no modifier. `--fdy-dl-col-min` (default `14rem`) is the
|
|
1586
|
+
narrowest a column may get.
|
|
1587
|
+
|
|
1588
|
+
```html
|
|
1589
|
+
<dl class="fdy-dl fdy-dl--grid">
|
|
1590
|
+
<div><dt>Customer</dt><dd>PT Sumber Makmur</dd></div>
|
|
1591
|
+
<div><dt>Email</dt><dd>procurement@sumbermakmur.co.id</dd></div>
|
|
1592
|
+
<div><dt>Phone</dt><dd class="fdy-mono">0812345678901</dd></div>
|
|
1593
|
+
</dl>
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
A fact is not a field: use this for read-only values, `.fdy-form-grid` only where something can be
|
|
1597
|
+
typed.
|
|
1437
1598
|
|
|
1438
1599
|
---
|
|
1439
1600
|
|
|
1440
1601
|
# Feedback
|
|
1441
1602
|
|
|
1603
|
+
## Copy a value — `[data-fdy-copy]`
|
|
1604
|
+
A button that copies a value the reader takes somewhere else: an external document number, an
|
|
1605
|
+
event ID. Needs `freeday-copy.js`; uses `Freeday.toast()` for its feedback.
|
|
1606
|
+
|
|
1607
|
+
```html
|
|
1608
|
+
<dd>
|
|
1609
|
+
<span class="fdy-mono">INV-2026-00042</span>
|
|
1610
|
+
<button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--icon fdy-btn--sm"
|
|
1611
|
+
data-fdy-copy="INV-2026-00042" aria-label="Copy invoice number"><!-- copy svg --></button>
|
|
1612
|
+
</dd>
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
- **The value is the attribute.** Write it from the same variable as the visible text; a table row
|
|
1616
|
+
needs no ids to find its own cell. An empty value copies nothing, so render the button only when
|
|
1617
|
+
there is something to copy.
|
|
1618
|
+
- One delegated listener on the document: a button rendered later needs no hydration.
|
|
1619
|
+
- The toast says `Copied.` on success. Without a Clipboard API (plain `http`, an old browser) or
|
|
1620
|
+
when the write is refused, a sticky danger toast tells the reader to copy by hand. A copy that
|
|
1621
|
+
silently does nothing is the one outcome it never produces. Override both strings per button
|
|
1622
|
+
with `data-fdy-text-copied` / `data-fdy-text-failed`.
|
|
1623
|
+
- Emits bubbling `fdy-copy` with `detail.value` and `detail.ok`.
|
|
1624
|
+
- Name the button for its value (`Copy invoice number`); an icon alone has no name.
|
|
1625
|
+
- Inside a clickable table row the click still reaches the row's handler. Stop it there if copying
|
|
1626
|
+
should not also open the row.
|
|
1627
|
+
|
|
1442
1628
|
## Alert — `.fdy-alert`
|
|
1443
1629
|
Inline message. Modifiers `--info` `--success` `--warning` `--danger`. Parts `__icon` `__body`
|
|
1444
1630
|
`__title` `__text` `__close`. The danger variant takes `role="alert"`.
|
|
@@ -1476,6 +1662,35 @@ something is in the top layer too. Where the Popover API is unavailable (before
|
|
|
1476
1662
|
Safari 17 / Firefox 125) the region falls back to a fixed block at `z-index: 200`, which cannot
|
|
1477
1663
|
clear an open modal, the message is still announced to a screen reader.
|
|
1478
1664
|
|
|
1665
|
+
## Confirm — `Freeday.confirm()`
|
|
1666
|
+
A yes/no question before an action that cannot be taken back. Imperative, like the toast. Needs
|
|
1667
|
+
`freeday-confirm.js`; renders a `.fdy-modal--sm`.
|
|
1668
|
+
|
|
1669
|
+
```js
|
|
1670
|
+
const yes = await Freeday.confirm({
|
|
1671
|
+
title: 'Delete order?',
|
|
1672
|
+
message: 'Order SO-1042 will be deleted permanently.',
|
|
1673
|
+
details: ['2 payments are deleted with it'], // optional, shown as a list
|
|
1674
|
+
acceptText: 'Delete order', rejectText: 'Keep', // name the action
|
|
1675
|
+
tone: 'danger', // 'primary' (default) | 'danger'
|
|
1676
|
+
})
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
- Resolves `true` on accept, `false` on reject, **Escape, or a click on the backdrop**. One dialog
|
|
1680
|
+
gives one answer.
|
|
1681
|
+
- `details` is for a confirm that has to name its findings. Five findings folded into `message`
|
|
1682
|
+
read as one long sentence.
|
|
1683
|
+
- Focus starts on the reject button, so an Enter that was meant for the page behind cannot
|
|
1684
|
+
confirm. Focus returns to the opener when it closes, and the dialog leaves the DOM.
|
|
1685
|
+
- Text is set as text, never parsed as HTML, so a record name can go into `message` as is.
|
|
1686
|
+
- The buttons carry `data-fdy-confirm-accept` / `data-fdy-confirm-reject`. A test needs them: an
|
|
1687
|
+
accept labelled "Delete" shares its accessible name with every row's own Delete button.
|
|
1688
|
+
- Defaults are `Confirm` / `Cancel`; pass `acceptText` / `rejectText` instead of relying on them.
|
|
1689
|
+
- Vue and React call it as is. Blazor awaits it through interop:
|
|
1690
|
+
`await JS.InvokeAsync<bool>("Freeday.confirm", new { title = "Delete order?", tone = "danger" })`.
|
|
1691
|
+
- An "unsaved changes" guard is three lines on top of it, in your router's leave hook:
|
|
1692
|
+
`return isDirty() ? Freeday.confirm({ … }) : true`.
|
|
1693
|
+
|
|
1479
1694
|
## Tooltip — `.fdy-tooltip`
|
|
1480
1695
|
Hover/focus only, never the sole carrier of information. Wrap in `.fdy-tooltip-wrap`; the trigger
|
|
1481
1696
|
gets `aria-describedby` → the `.fdy-tooltip[role="tooltip"]` `id`.
|
|
@@ -1610,6 +1825,12 @@ for primary navigation (see Nav above).
|
|
|
1610
1825
|
`.fdy-breadcrumb__item` (last one `aria-current="page"`, no link), `.fdy-breadcrumb__link`, and
|
|
1611
1826
|
`.fdy-breadcrumb__sep` marked `aria-hidden="true"`.
|
|
1612
1827
|
|
|
1828
|
+
`aria-current="page"` belongs to the crumb that **is** this page, not to whichever crumb is last. A
|
|
1829
|
+
trail that stops at the parent (a record form whose last crumb is its list) keeps that crumb a
|
|
1830
|
+
link with no `aria-current`; otherwise the reader is told they are on the list while looking at a
|
|
1831
|
+
record. A crumb for a menu group that has no page of its own is plain text in the item, not a link
|
|
1832
|
+
to nowhere.
|
|
1833
|
+
|
|
1613
1834
|
## Tabs — `.fdy-tabs`
|
|
1614
1835
|
APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
1615
1836
|
`.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
|
|
@@ -1736,7 +1957,7 @@ is `role="group" aria-roledescription="slide"`.
|
|
|
1736
1957
|
# Display
|
|
1737
1958
|
|
|
1738
1959
|
## Card — `.fdy-card`
|
|
1739
|
-
Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
1960
|
+
Parts `__head` `__body` `__title` `__desc` `__action` `__footer`. Modifiers:
|
|
1740
1961
|
|
|
1741
1962
|
- `--elevated` raises it (use sparingly; most surfaces are flat)
|
|
1742
1963
|
- `--interactive` gives cursor + hover-lift to a card that *has* a click handler. **Presentational
|
|
@@ -1753,6 +1974,26 @@ Parts `__body` `__title` `__desc` `__footer`. Modifiers:
|
|
|
1753
1974
|
- The card is `position:relative`, so a badge or ribbon you absolutely position inside it anchors
|
|
1754
1975
|
to the card. That is also what keeps hidden labels inside it from escaping. See *Containment*.
|
|
1755
1976
|
|
|
1977
|
+
**Header band: `__head`.** A mark, the title and the card's way out on one row, with a rule under
|
|
1978
|
+
it from edge to edge. The action sits beside the title so it is in the same place on every card;
|
|
1979
|
+
under the content it moved each time the content grew. The title truncates rather than wrapping
|
|
1980
|
+
under the action. The mark is an `.fdy-avatar --sm --square --tone-N`, so its tints are the ones
|
|
1981
|
+
the contrast gate already covers; pick the tone for what the card is **about** and keep it fixed.
|
|
1982
|
+
|
|
1983
|
+
```html
|
|
1984
|
+
<article class="fdy-card">
|
|
1985
|
+
<header class="fdy-card__head">
|
|
1986
|
+
<span class="fdy-avatar fdy-avatar--sm fdy-avatar--square fdy-avatar--tone-3" aria-hidden="true"><!-- icon --></span>
|
|
1987
|
+
<h3 class="fdy-card__title">Due today</h3>
|
|
1988
|
+
<div class="fdy-card__action"><a class="fdy-btn fdy-btn--text fdy-btn--sm" href="/queue">View all</a></div>
|
|
1989
|
+
</header>
|
|
1990
|
+
<div class="fdy-card__body">…</div>
|
|
1991
|
+
</article>
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
A collapsible section whose band is a `<button>` cannot hold the action inside it (a button in a
|
|
1995
|
+
button is invalid and is read as one control): put `__action` beside the button, not in it.
|
|
1996
|
+
|
|
1756
1997
|
### One card, one primary action, one escape hatch
|
|
1757
1998
|
|
|
1758
1999
|
A card with **two** actions cannot be `--button`: interactive content nested in a `<button>` is
|
|
@@ -1813,6 +2054,8 @@ Initials or image. Sizes `--sm` / default / `--lg`; `.fdy-avatar-group` stacks t
|
|
|
1813
2054
|
group). `--tone-1`…`--tone-8` are decorative tints (WCAG AA in both themes) to distinguish
|
|
1814
2055
|
same-initial avatars. Hash the index off the **full** name, not the initials.
|
|
1815
2056
|
|
|
2057
|
+
`--square` for a mark that is not a person: a company logo, a card's icon chip.
|
|
2058
|
+
|
|
1816
2059
|
Sizes: `--xs` 1.5rem · *(base)* 2.5rem · `--sm` 2rem · `--lg` 3.5rem. Use **`--xs` inside a control**
|
|
1817
2060
|
because `.fdy-btn--sm` is itself 2rem tall, so an `--sm` avatar fills a small button edge to edge and the
|
|
1818
2061
|
ghost border cuts across the circle.
|
package/README.id.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
|
|
6
6
|
|
|
7
7
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.6.0)
|
|
9
9
|
|
|
10
10
|
UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
|
|
11
11
|
spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
|
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
|
[](https://cahyo-dimas.github.io/freeday-ui-kit/)
|
|
8
|
-
[](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.6.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`.
|
|
@@ -13,6 +13,9 @@ public partial class FdyDateRange
|
|
|
13
13
|
[Parameter] public string? To { get; set; }
|
|
14
14
|
[Parameter] public EventCallback<string?> ToChanged { get; set; }
|
|
15
15
|
|
|
16
|
+
/// <summary>Both ends chosen, or both cleared: the only states worth reloading a list for (#063).</summary>
|
|
17
|
+
[Parameter] public EventCallback<SettledRange> Settled { get; set; }
|
|
18
|
+
|
|
16
19
|
[Parameter] public string? AriaLabel { get; set; }
|
|
17
20
|
[Parameter] public string? FromPlaceholder { get; set; }
|
|
18
21
|
[Parameter] public string? ToPlaceholder { get; set; }
|
|
@@ -44,6 +47,10 @@ public partial class FdyDateRange
|
|
|
44
47
|
To = detail.Value;
|
|
45
48
|
await ToChanged.InvokeAsync(detail.Value);
|
|
46
49
|
}
|
|
50
|
+
if (string.IsNullOrEmpty(From) == string.IsNullOrEmpty(To))
|
|
51
|
+
{
|
|
52
|
+
await Settled.InvokeAsync(new SettledRange(From, To));
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
protected override async ValueTask CleanupAsync()
|
|
@@ -55,4 +62,7 @@ public partial class FdyDateRange
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
public sealed record RangeDetail(string? Value, string? Role);
|
|
65
|
+
|
|
66
|
+
/// <summary>The range as it stands when it settles; both empty means "no period".</summary>
|
|
67
|
+
public sealed record SettledRange(string? From, string? To);
|
|
58
68
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
<div class="fdy-table-scroll">
|
|
33
|
-
<table class="fdy-table @(Striped ? "fdy-table--striped" : null)" aria-label="@AriaLabel">
|
|
33
|
+
<table class="fdy-table @(Striped ? "fdy-table--striped" : null) @(HoverActionable ? "fdy-table--hover-actionable" : null)" aria-label="@AriaLabel">
|
|
34
34
|
<thead>
|
|
35
35
|
<tr>
|
|
36
36
|
@if (Selectable)
|
|
@@ -77,7 +77,11 @@
|
|
|
77
77
|
{
|
|
78
78
|
<tr>
|
|
79
79
|
<td colspan="@ColSpan" class="fdy-table__state">
|
|
80
|
-
@if (
|
|
80
|
+
@if (EmptyTemplate is not null)
|
|
81
|
+
{
|
|
82
|
+
@EmptyTemplate(new FdyTableEmptyContext(Filtered, ClearFiltersAsync))
|
|
83
|
+
}
|
|
84
|
+
else if (EmptyContent is not null)
|
|
81
85
|
{
|
|
82
86
|
@EmptyContent
|
|
83
87
|
}
|
|
@@ -71,6 +71,9 @@ public partial class FdyTable<TRow>
|
|
|
71
71
|
[Parameter] public string LoadingText { get; set; } = "Loading…";
|
|
72
72
|
[Parameter] public string EmptyText { get; set; } = "No data";
|
|
73
73
|
[Parameter] public RenderFragment? EmptyContent { get; set; }
|
|
74
|
+
/// <summary>The empty state, told whether the column filters emptied the table and how to clear
|
|
75
|
+
/// them (#069). Wins over <see cref="EmptyContent"/> when both are set.</summary>
|
|
76
|
+
[Parameter] public RenderFragment<FdyTableEmptyContext>? EmptyTemplate { get; set; }
|
|
74
77
|
[Parameter] public string? AriaLabel { get; set; }
|
|
75
78
|
|
|
76
79
|
/// <summary>Opt in to row activation: rows become focusable and raise <see cref="RowActivate"/>.</summary>
|
|
@@ -90,6 +93,9 @@ public partial class FdyTable<TRow>
|
|
|
90
93
|
/// <summary>Zebra-stripe the body rows.</summary>
|
|
91
94
|
[Parameter] public bool Striped { get; set; }
|
|
92
95
|
|
|
96
|
+
/// <summary>Hover only the rows that do something (<c>RowActivatable</c>); inert rows keep their colour.</summary>
|
|
97
|
+
[Parameter] public bool HoverActionable { get; set; }
|
|
98
|
+
|
|
93
99
|
/// <summary>Render the checkbox column and the bulk bar.</summary>
|
|
94
100
|
[Parameter] public bool Selectable { get; set; }
|
|
95
101
|
|
|
@@ -234,6 +240,21 @@ public partial class FdyTable<TRow>
|
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
private bool Filtered => EffectiveFilters.Values.Any(TableModel.IsFilterActive);
|
|
244
|
+
|
|
245
|
+
private async Task ClearFiltersAsync()
|
|
246
|
+
{
|
|
247
|
+
if (FiltersControlled)
|
|
248
|
+
{
|
|
249
|
+
await FiltersChanged.InvokeAsync(new Dictionary<string, FdyColumnFilter>());
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
{
|
|
253
|
+
_internalFilters = new();
|
|
254
|
+
await SetClientPageAsync(0);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
237
258
|
private async Task OnFilterChange(FdyTableColumn<TRow> col, FdyColumnFilter? filter)
|
|
238
259
|
{
|
|
239
260
|
Dictionary<string, FdyColumnFilter> next = new(EffectiveFilters);
|
|
@@ -21,6 +21,10 @@ public sealed record FdySortState(string Key, FdySortDir Dir);
|
|
|
21
21
|
/// <see cref="FdyTable{TRow}.Page"/> switches the table into server mode.</summary>
|
|
22
22
|
public sealed record FdyPageState(int Index, int Size, int Total);
|
|
23
23
|
|
|
24
|
+
/// <summary>What <c>EmptyTemplate</c> is told (#069): whether an active column filter emptied the
|
|
25
|
+
/// table, and how to clear every filter.</summary>
|
|
26
|
+
public sealed record FdyTableEmptyContext(bool Filtered, Func<Task> ClearFilters);
|
|
27
|
+
|
|
24
28
|
/// <summary>The processed page of rows (after filter/sort/paginate) plus the total row count, as
|
|
25
29
|
/// raised by <see cref="FdyTable{TRow}.Process"/>. Lets a consumer render the same processed set
|
|
26
30
|
/// somewhere else, a card list below the <c>md</c> breakpoint, a summary, an export, without
|
|
@@ -15,6 +15,8 @@ export interface DateRangeValue {
|
|
|
15
15
|
export interface FdyDateRangeProps {
|
|
16
16
|
value: DateRangeValue;
|
|
17
17
|
onChange: (value: DateRangeValue) => void;
|
|
18
|
+
/** Both ends chosen, or both cleared: the only states worth reloading a list for (#063). */
|
|
19
|
+
onSettled?: (value: DateRangeValue) => void;
|
|
18
20
|
min?: string;
|
|
19
21
|
max?: string;
|
|
20
22
|
locale?: string;
|
|
@@ -30,8 +32,12 @@ export interface FdyDateRangeProps {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function FdyDateRange(props: FdyDateRangeProps): JSX.Element {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
+
const commit = (next: DateRangeValue): void => {
|
|
36
|
+
props.onChange(next);
|
|
37
|
+
if (!next.start === !next.end) props.onSettled?.(next);
|
|
38
|
+
};
|
|
39
|
+
const onStart = (start: string): void => commit({ start, end: props.value.end });
|
|
40
|
+
const onEnd = (end: string): void => commit({ start: props.value.start, end });
|
|
35
41
|
|
|
36
42
|
return (
|
|
37
43
|
<div
|