@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 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? Drop the type back to `text` and add a `.fdy-input-group__btn` that clears. |
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">&times;</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
  [![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.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
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)
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
  [![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.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
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)
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 (EmptyContent is not null)
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 onStart = (start: string): void => props.onChange({ start, end: props.value.end });
34
- const onEnd = (end: string): void => props.onChange({ start: props.value.start, end });
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