@cahyo-dimas/freeday 3.1.0 → 3.3.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 +151 -0
- package/COMPONENTS.md +68 -7
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/USAGE.md +10 -0
- package/adapters/blazor/FdyCfl.razor +24 -18
- package/adapters/blazor/FdyCfl.razor.cs +13 -2
- package/adapters/blazor/FdyModal.razor.cs +1 -1
- package/adapters/react/components/FdyCfl.tsx +79 -44
- package/adapters/react/components/FdyModal.tsx +2 -1
- package/adapters/react/index.d.ts +1 -1
- package/adapters/vue/components/FdyCfl.vue +56 -40
- package/adapters/vue/components/FdyModal.vue +4 -1
- package/dist/freeday.bundle.css +106 -10
- package/dist/freeday.css +104 -8
- package/dist/freeday.tokens.css +2 -2
- package/docs/agent-onboarding.md +6 -0
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/components/app-shell.css +10 -3
- package/src/components/cfl.css +66 -2
- package/src/components/input.css +10 -0
- package/src/components/tabs.css +18 -3
- package/tokens/tokens.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,157 @@ 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.3.0] - 2026-08-28
|
|
13
|
+
|
|
14
|
+
**Tiga kemampuan yang sebenarnya sudah ada di kit ini, dan tak satu pun bisa diminta.** `#054` dan
|
|
15
|
+
`#055` datang dari tiga app berbeda dan bermuara pada satu bentuk kegagalan yang sama: kemampuannya
|
|
16
|
+
ada — di jalur mentah, di satu container, di satu varian — tapi **kosakata untuk memintanya tidak**.
|
|
17
|
+
Itu jenis gap yang paling mahal, karena tak terlihat seperti gap: yang membacanya menyimpulkan kit
|
|
18
|
+
tak bisa, lalu membangun ulang. Satu di antaranya membuat dua console menulis 23 override yang sama,
|
|
19
|
+
satu lagi membuat sebuah panel menyimpan dialog 420 baris di sebelah komponen yang melakukan hal itu
|
|
20
|
+
persis, dan yang ketiga bertahan empat bulan hanya karena satu kalimat dokumentasi salah.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`FdyCfl` bisa dibuka oleh sesuatu yang bukan field-nya sendiri** (`#054`, dari
|
|
24
|
+
`IDU_AI_DOC_SAPB1_ADDON_DESKTOP_APP`). Enhancer punya `[data-fdy-cfl-open]` — *"opens the dialog
|
|
25
|
+
with no bound field"* — sejak ia terbit, dan COMPONENTS.md mendaftarnya. Ketiga wrapper typed
|
|
26
|
+
adalah **field plus dialog** dengan `openDialog` terkunci di dalam: tak ada ref, tak ada prop, tak
|
|
27
|
+
ada pintu. Jadi jalur mentah bisa melakukan hal yang jalur typed tidak — satu-satunya asimetri yang
|
|
28
|
+
dilarang klaim paritas kit ini. Panel 420px yang melaporkannya memicu picker dari **chip 12px**,
|
|
29
|
+
dua per baris dokumen; ia tak bisa membelanjakan satu readonly input 22rem per pemicu, jadi ia
|
|
30
|
+
menyimpan `CflGridDialog.vue` 420 baris. Sekarang: prop **`dialogOnly`** merender dialog tanpa
|
|
31
|
+
field, dan **`open()`/`close()`** terjangkau — Vue lewat `defineExpose`, React lewat
|
|
32
|
+
`FdyCflHandle` di `ref`, Blazor lewat `OpenAsync()`/`CloseAsync()` yang kini `public`. Host-nya
|
|
33
|
+
`display:contents` (`.fdy-cfl__host`), jadi ia **tak menghasilkan box sama sekali**: div berukuran
|
|
34
|
+
nol pun tetap jadi flex item atau sel grid di tempat pemicunya diletakkan, dan itu persis yang
|
|
35
|
+
akan merusak barisan chip. Blazor tak butuh host — ia memang sudah merender field dan dialog
|
|
36
|
+
sebagai sibling. `disabled`/`readonly` tetap menolak membuka, karena tanpa field tak ada tombol
|
|
37
|
+
disabled yang menahan klik: penjaganya harus ada di `open()` sendiri, dan itu yang diuji.
|
|
38
|
+
- **`.fdy-field--full` melepas cap 22rem di mana pun field itu berada** (`#055` §1, dari
|
|
39
|
+
`IDU_AI_DOC_SAPB1_CLIENT` **dan** `IDU_AI_DOC_SAAS` — dua consumer independen yang meraih override
|
|
40
|
+
yang sama adalah buktinya, jumlahnya adalah argumennya). Kit sudah mengirim idenya:
|
|
41
|
+
`.fdy-form-grid > .fdy-field { max-width: none }`. Tapi setiap form nyata adalah **flex column**,
|
|
42
|
+
bukan grid itu — `.fdy-form-grid` adalah `repeat(auto-fit,minmax(14rem,1fr))`, yang memasangkan
|
|
43
|
+
field berdua begitu ada ruang. Benar untuk header dokumen, salah untuk settings pane, kartu login,
|
|
44
|
+
atau dialog 420px. Dan di luar grid, modifier-nya bukan sekadar "tak melepas cap": **ia tak
|
|
45
|
+
melakukan apa-apa**, satu-satunya rule-nya ber-scope grid. Jadi form kolom tunggal tak punya cara
|
|
46
|
+
mengatakan maksudnya, dan keduanya mengatakannya di container — 23 blok. Kit ini pun menulisnya
|
|
47
|
+
**empat kali di halaman referensinya sendiri** sebagai `style="max-width:none"`; keempatnya kini
|
|
48
|
+
`fdy-field--full`. Di dalam grid tak ada yang berubah (rule grid sudah mengatakannya), dan
|
|
49
|
+
`grid-column` tetap urusan grid.
|
|
50
|
+
- **`--fdy-app-sidebar-w`** (default `15.5rem`) di `.fdy-app__sidebar`. Kedua console meng-override
|
|
51
|
+
`width`-nya ke `16.5rem` — yaitu meng-override rule yang juga menganimasikan collapse.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- **Shell bisa memuat anak yang menggulung** (`#055` §2). `.fdy-app__content` punya `min-width:0`
|
|
55
|
+
dan **tidak** `min-height:0`. Di flex column, sumbu **block**-lah yang menjebak overflow: automatic
|
|
56
|
+
minimum size sebuah flex item adalah kontennya, jadi consumer yang memaku shell ke viewport lalu
|
|
57
|
+
menggulung `.fdy-app__main` mendorong shell-nya terbuka, bukan menggulung. Topbar cerita yang sama
|
|
58
|
+
satu baris di bawah: flex item tanpa `flex`, jadi topbar yang kontennya membungkus melewati
|
|
59
|
+
`min-height` bisa terperas. Kedua console menulis kedua rule itu sendiri-sendiri, di `app.css`,
|
|
60
|
+
sebelum layar kelima mereka. Keduanya kini di kit; tak ada layout yang hari ini benar yang berubah.
|
|
61
|
+
- **Kotak pencarian `.fdy-cfl__search` tak lagi ter-cap 22rem.** Empat berkas menulis ini sebagai
|
|
62
|
+
`style="max-width:none"` — kedua adapter typed dan kedua contoh docs — yaitu kosakata yang hilang
|
|
63
|
+
dari `#055` §1, satu class di sebelahnya. Sekarang satu rule, nol inline style.
|
|
64
|
+
|
|
65
|
+
### Docs
|
|
66
|
+
- **`.fdy-nav--flat` akhirnya dijelaskan sebagai dirinya sendiri** (`#055` §3, dan ini temuan yang
|
|
67
|
+
lebih berharga daripada item aslinya). Catatan itu meminta "bentuk resmi untuk nav grup statis".
|
|
68
|
+
Bentuk itu **sudah ada sejak 1.1.0** dan melakukan persis tiga hal yang kedua console tulis
|
|
69
|
+
tangan: caret hilang, `cursor:default`, pemisah antar-grup hilang — plus dukungan markup
|
|
70
|
+
non-`<details>`, yang persis kasus `<p class="fdy-nav__grouplabel">` mereka. Yang menyembunyikannya
|
|
71
|
+
adalah satu kalimat di COMPONENTS.md: *"`--flat` drops the surface"*, yang **tidak benar** —
|
|
72
|
+
`.fdy-nav` tak mengecat surface apa pun untuk dibuang. Deskripsi yang akurat hanya hidup di entri
|
|
73
|
+
CHANGELOG 1.1.0. Dua consumer membaca satu kalimat yang salah, menyimpulkan varian itu bukan
|
|
74
|
+
untuk mereka, lalu membangun ulang sesuatu yang sudah mereka punya, selama empat bulan.
|
|
75
|
+
**Pelajarannya bukan tentang nav:** deskripsi varian yang salah lebih mahal daripada varian yang
|
|
76
|
+
hilang, karena yang hilang menghasilkan laporan dan yang salah menghasilkan pekerjaan diam-diam.
|
|
77
|
+
- `COMPONENTS.md`: `.fdy-field--full` kini dijelaskan sebagai pelepas cap (bukan hanya span kolom),
|
|
78
|
+
plus paragraf "single-column form is not a grid"; tabel props `<FdyCfl>` dapat `dialogOnly` dan
|
|
79
|
+
satu bagian tentang membuka picker lewat ref; `--fdy-app-sidebar-w` didaftar di App shell.
|
|
80
|
+
|
|
81
|
+
### Guarded
|
|
82
|
+
- **Empat gerbang baru, semuanya diverifikasi dengan mutasi.** `test/css.test.mjs`: `--full` melepas
|
|
83
|
+
cap **dan** berada setelah `.fdy-field` (spesifisitas sama, jadi urutan sumber adalah seluruh
|
|
84
|
+
mekanismenya); shell punya kedua `min-*` dan topbar punya `flex:none`. `test/docs.test.mjs`:
|
|
85
|
+
paritas `#054` di tiga stack **plus** hook `[data-fdy-cfl-open]` di enhancer — kalau hook itu
|
|
86
|
+
hilang, gerbangnya membandingkan dengan ketiadaan. `browser/cfl-dialog-only.mjs`: 4 test Chrome
|
|
87
|
+
nyata (Vue + React), dibuka lewat **klik sungguhan** pada chip milik app, bukan dengan memanggil
|
|
88
|
+
method — plus satu asersi yang mengukur **akibat** dan bukan CSS: fixture-nya flex row bergap 8px
|
|
89
|
+
dengan komponen di antara dua chip, jadi host yang punya box terbaca sebagai 16px. Asersi itu juga
|
|
90
|
+
yang menangkap kesalahan pertama penulisnya: hitungan `.fdy-input-group` yang naif menghitung
|
|
91
|
+
**kotak pencarian di dalam dialog**, bukan field.
|
|
92
|
+
|
|
93
|
+
## [3.2.0] - 2026-08-28
|
|
94
|
+
|
|
95
|
+
**Dua cacat yang tak bisa di-screenshot, dan satu komponen yang akhirnya muat di panel 420px.**
|
|
96
|
+
Ketiganya datang dari layar sungguhan. Yang menyatukannya: tak satu pun terlihat di DOM atau di
|
|
97
|
+
stylesheet — yang satu hanya bergerak, yang satu hanya terbaca, yang satu hanya muat atau tidak.
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
- **Strip tab tak lagi menggulung vertikal, dan garis aktifnya kini benar-benar 2px** (`#052`, dari
|
|
101
|
+
`IDU_AI_DOC_SAPB1_CLIENT`). Dilaporkan pengguna yang bisa merasakannya tapi tak bisa memotretnya:
|
|
102
|
+
*"panel tab ini menggulung vertikal, susah di-screenshot."* Memang begitu — **1px**.
|
|
103
|
+
`overflow-x:auto` tak bisa meminta satu sumbu saja (CSS Overflow §3: sumbu lain yang `visible`
|
|
104
|
+
ikut jadi `auto`), jadi 1px yang digantungkan `margin-bottom:-1px` milik tab — yang ada justru
|
|
105
|
+
untuk mengangkat underline 2px-nya ke atas garis strip — berubah jadi scrollport sungguhan yang
|
|
106
|
+
memakan gestur trackpad. Yang tak dilaporkan siapa pun, dan hanya piksel yang bisa membedakannya:
|
|
107
|
+
scroll container **memotong** keturunannya di padding box, jadi separuh bawah underline itu ikut
|
|
108
|
+
terpotong. Underline aktif terkirim **1px** selama negative margin itu ada — CSS-nya bilang 2px,
|
|
109
|
+
DOM-nya bilang 2px, layarnya bilang 1. Garis strip kini `box-shadow` inset (dicat di kotaknya,
|
|
110
|
+
bukan di konten yang menggulung), dan tab tak lagi menggantung. **Bagi konsumen yang menyembunyikan
|
|
111
|
+
garis itu:** `border-bottom:0` tak lagi menjangkaunya, pakai `box-shadow:none`. Sapuan ke seluruh
|
|
112
|
+
scroller lain (`.fdy-table-scroll`, `.fdy-nav--horizontal`, `.fdy-carousel__viewport`) bersih —
|
|
113
|
+
`.fdy-tabs__list` satu-satunya.
|
|
114
|
+
- **`--color-text-subtle` gelap naik ke `slate-450`** (`#053` §2, dari
|
|
115
|
+
`IDU_AI_DOC_SAPB1_ADDON_DESKTOP_APP`). 4.02:1 di `--color-surface` gelap, di bawah AA, dan yang
|
|
116
|
+
memakainya bukan cuma app: **placeholder kit sendiri** (`.fdy-datepicker__value--placeholder` dan
|
|
117
|
+
sebelas `.fdy-cal__day.is-outside` dalam satu kalender). Yang menarik, tokennya tak melanggar
|
|
118
|
+
kontraknya — gerbang kontras memang menguji tier ini di **3:1 sebagai non-teks**; yang salah
|
|
119
|
+
adalah kit memakai tier dekoratif untuk teks yang berutang AA. Sekarang **4.88:1** di
|
|
120
|
+
`--color-surface`, yaitu permukaan yang dicat setiap `.fdy-input`, jadi placeholder lolos di tempat
|
|
121
|
+
placeholder benar-benar duduk. Light tak disentuh (sudah 4.69). **AA di ketiga permukaan gelap
|
|
122
|
+
sengaja tidak dikejar**: mencapainya butuh nada sejengkal dari `slate-400`, yang *adalah*
|
|
123
|
+
`--color-text-muted`, dan subtle yang sama dengan muted bukan tier lagi.
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
- **`FdyCfl` punya layout sempit, tanpa satu pun prop baru** (`#053` §1). Di bawah **30rem**,
|
|
127
|
+
barisnya menata diri sebagai list item: kolom pertama jadi judul, sisanya satu baris detail di
|
|
128
|
+
bawahnya, `<thead>` dilepas. **Container query, bukan media query** — dialog bukan viewport, dan
|
|
129
|
+
komponen yang sama adalah tabel di halaman desktop sekaligus daftar di dalam panel 420px yang
|
|
130
|
+
ditambatkan di SAP Business One. Kontrak datanya tak berubah: `columns` sudah menyebut kolom mana
|
|
131
|
+
yang identitas. Keempat stack dapat sekaligus, karena yang bekerja CSS.
|
|
132
|
+
- **`size="cfl"` di `FdyModal`** (`#053` §4), Vue · React · Blazor. `.fdy-modal--cfl` (46rem) sudah
|
|
133
|
+
ada di CSS sejak lama tapi tak terjangkau dari adapter mana pun; `FdyCfl` memasang kelasnya
|
|
134
|
+
sendiri, jadi celah ini hanya muncul bagi yang membangun dialog sendiri selebar itu. **Bukan
|
|
135
|
+
breaking** meski union publiknya melebar: kebijakan 2.1.0 menyasar tipe di posisi *keluaran*
|
|
136
|
+
(handler yang menerima nilai lebih lebar); ini masukan, dan pemanggil yang mengirim nilai lama
|
|
137
|
+
tetap valid.
|
|
138
|
+
|
|
139
|
+
### Added: guards
|
|
140
|
+
- `browser/tabs-strip.mjs` — nol overflow vertikal, underline 2px terbaca dari **piksel** yang
|
|
141
|
+
benar-benar dicat, dan gulir horizontal yang tetap hidup. Diverifikasi mutasi: dengan aturan 3.1.0
|
|
142
|
+
dikembalikan, 2 dari 3 merah.
|
|
143
|
+
- `browser/cfl-narrow.mjs` — menjalankan `FdyCfl.vue` sungguhan di viewport **420px**, lebar
|
|
144
|
+
konsumen yang melaporkannya, karena container query menanyakan lebar *dialog* dan hanya dialog
|
|
145
|
+
sungguhan yang punya lebar.
|
|
146
|
+
- `test/contrast.test.mjs` dapat pasangan baru khusus placeholder, terpisah dari tier dekoratifnya,
|
|
147
|
+
jadi kedua paruh keputusan itu dijaga: gagal di 4.02 kalau tokennya dikembalikan, dan tetap 3:1
|
|
148
|
+
untuk chevron/separator.
|
|
149
|
+
|
|
150
|
+
### Docs
|
|
151
|
+
- `COMPONENTS.md`: kontrak override garis strip tab, layout sempit `FdyCfl`, dan `size="cfl"`.
|
|
152
|
+
- `USAGE.md`: `.fdy-text-subtle` akhirnya masuk tabel peran teks — tabel itu berhenti di "muted",
|
|
153
|
+
padahal kelasnya publik — beserta kontrak kontrasnya di kedua tema.
|
|
154
|
+
|
|
155
|
+
### Tidak diperbaiki, dan alasannya
|
|
156
|
+
- **`--color-text-muted` 3.94:1 di `--color-surface-raised`** (`#053` §3) **tidak reproduksi.** Di
|
|
157
|
+
`soft`, `--color-surface-raised` *identik* dengan `--color-surface` (7.28:1); di `glass`,
|
|
158
|
+
komposit terburuknya 6.70:1; dan muted tak pernah turun di bawah **5.46:1** di mana pun dalam ramp
|
|
159
|
+
gelap. Angka 3.94 itu sendiri nyata: ia persis `slate-400` di atas `slate-700`, yaitu
|
|
160
|
+
`--color-border-strong` — token border yang tak dipakai komponen mana pun sebagai latar. Item ini
|
|
161
|
+
dibiarkan **open** dengan pertanyaan balik ke pelapor, bukan ditutup sebagai withdrawn.
|
|
162
|
+
|
|
12
163
|
## [3.1.0] - 2026-08-27
|
|
13
164
|
|
|
14
165
|
**Dua cacat lebar yang dilaporkan dari layar sungguhan, dan keduanya tak kelihatan di DOM.** Markup-nya
|
package/COMPONENTS.md
CHANGED
|
@@ -142,6 +142,8 @@ off-canvas drawer and backdrop are built in.
|
|
|
142
142
|
|
|
143
143
|
- Parts: `__sidebar` `__brand` (`__brand-mark` `__brand-text` `__brand-title` `__brand-subtitle`)
|
|
144
144
|
`__content` `__topbar` `__navtoggle` `__title` `__main` `__backdrop`
|
|
145
|
+
- Sidebar width: `--fdy-app-sidebar-w` on `.fdy-app` (default `15.5rem`). Set it rather than
|
|
146
|
+
overriding `width` on `.fdy-app__sidebar`, whose value also feeds the collapse transition.
|
|
145
147
|
- Modifiers: `--nav-open` (drawer open) · `--nav-collapsed` (collapse to zero width, ≥721px) ·
|
|
146
148
|
`--nav-overlay` (≥721px: float the nav over the content instead of displacing it) · `--static`
|
|
147
149
|
(embed the shell in a page instead of filling the viewport)
|
|
@@ -422,15 +424,22 @@ WAI-ARIA APG menu-button. Keyboard ↑/↓/Home/End/Esc handled by the enhancer.
|
|
|
422
424
|
The field wrapper owns the vertical rhythm; `<label class="fdy-field">` when it wraps a single
|
|
423
425
|
native control, otherwise a `<div>` + explicitly associated label.
|
|
424
426
|
|
|
425
|
-
- `.fdy-field` (+ `--full
|
|
426
|
-
`--w-
|
|
427
|
+
- `.fdy-field` (+ `--full`, which releases the width cap **wherever the field is**, and inside
|
|
428
|
+
`.fdy-form-grid` also spans the row; widths `--w-sm` `--w-lg` `--w-xl` `--w-2xl` `--w-grow`
|
|
429
|
+
inside `.fdy-filterbar`)
|
|
427
430
|
- **The field owns the width; the control in it follows.** `.fdy-field` caps at `22rem` so a lone
|
|
428
|
-
field does not run the width of the page. `.fdy-form-grid` and `.fdy-filterbar` lift that cap,
|
|
429
|
-
so does any width you state yourself — and in every one of those cases the control (`.fdy-input`,
|
|
431
|
+
field does not run the width of the page. `.fdy-form-grid` and `.fdy-filterbar` lift that cap,
|
|
432
|
+
`.fdy-field--full` lifts it on one field anywhere, and so does any width you state yourself — and in every one of those cases the control (`.fdy-input`,
|
|
430
433
|
`.fdy-input-group`, `.fdy-combo`, `.fdy-autocomplete`, `.fdy-cascade`, and the pickers) spans the
|
|
431
434
|
field. So **size the field, never the control**: styling the control instead leaves the field
|
|
432
435
|
wider than what is in it, and that dead space is invisible in the DOM — it reads on screen as a
|
|
433
436
|
gap you did not write.
|
|
437
|
+
- **A single-column form is not a grid.** `.fdy-form-grid` pairs fields up the moment there is room,
|
|
438
|
+
which is right for a document header and wrong for a settings pane, a login card, or a dialog at
|
|
439
|
+
420px. Those are a plain flex column, and the way one says *"these fields fill me"* is
|
|
440
|
+
`.fdy-field--full` on the fields — **not** a `max-width` override on the container. Both of this
|
|
441
|
+
kit's own consoles wrote that override, once per form, before this modifier reached outside the
|
|
442
|
+
grid.
|
|
434
443
|
- `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
|
|
435
444
|
|
|
436
445
|
**Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
|
|
@@ -640,6 +649,15 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
|
640
649
|
## Choose-from-list (CFL) — `data-fdy-cfl`
|
|
641
650
|
> **Typed wrapper: `<FdyCfl>`**. 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).
|
|
642
651
|
|
|
652
|
+
> **Narrow: the results stack, and there is no prop for it** (`#053`). Under a `.fdy-cfl__results`
|
|
653
|
+
> narrower than **30rem** — a docked add-on panel, a phone — the rows lay out as list items instead
|
|
654
|
+
> of table cells: the first column becomes the title, the remaining columns become one detail line
|
|
655
|
+
> under it, and `<thead>` is dropped. It is a **container** query, so what decides is the dialog's
|
|
656
|
+
> width and not the viewport's; the same component is a table on a desktop page and a list inside a
|
|
657
|
+
> 420px panel, with no flag to pass and nothing to keep in sync. Your data contract does not change:
|
|
658
|
+
> `columns`, `display` and `rowKey` already say which column is the identity. If a screen genuinely
|
|
659
|
+
> needs columns at that width, override the `@container` block — do not widen the dialog.
|
|
660
|
+
|
|
643
661
|
### Props — `<FdyCfl>`
|
|
644
662
|
|
|
645
663
|
Four of these are required and carry the whole component: `fetchPage`, `columns`, `display`,
|
|
@@ -665,6 +683,31 @@ Four of these are required and carry the whole component: `fetchPage`, `columns`
|
|
|
665
683
|
| `clearLabel?` | `string` | Accessible name for that button. Default `Clear selection`. |
|
|
666
684
|
| `disabled?` · `readonly?` · `invalid?` | `boolean` | `readonly` keeps the picked value visible, focusable and copyable, but the dialog cannot be opened. |
|
|
667
685
|
| `id?` · `ariaLabelledby?` · `describedby?` | `string` | Field id; the element that labels it; the help/error text it points at. |
|
|
686
|
+
| `dialogOnly?` | `boolean` | Render the **dialog and no field**, for a caller whose trigger is already its own. See below. |
|
|
687
|
+
|
|
688
|
+
**Opening it yourself.** The field is one way in, not the only one. A ref gives you `open()` and
|
|
689
|
+
`close()` — Vue `defineExpose`, React a `FdyCflHandle` through `ref`, Blazor `OpenAsync()` /
|
|
690
|
+
`CloseAsync()` on the `@ref` — and `dialogOnly` drops the field entirely so the picker can be
|
|
691
|
+
triggered by a chip, a table cell, a menu item, or anything else that is not a 22rem readonly input.
|
|
692
|
+
This is what the raw path has always had in `[data-fdy-cfl-open]`; before 3.3.0 the typed wrappers
|
|
693
|
+
were a field *plus* a dialog with no door, and a screen that could not spend a field per trigger had
|
|
694
|
+
to hand-roll the whole picker. With `dialogOnly` the component's host generates no box
|
|
695
|
+
(`.fdy-cfl__host` is `display:contents`), so it can be rendered anywhere — Blazor renders only the
|
|
696
|
+
`<dialog>` and needs no host. `placeholder`, `clearable`, `id` and the field's aria props have
|
|
697
|
+
nothing to name in that mode and are ignored; `disabled` and `readonly` still refuse to open, so a
|
|
698
|
+
programmatic `open()` cannot bypass them.
|
|
699
|
+
|
|
700
|
+
```vue
|
|
701
|
+
<script setup lang="ts">
|
|
702
|
+
const picker = ref<InstanceType<typeof FdyCfl> | null>(null);
|
|
703
|
+
</script>
|
|
704
|
+
|
|
705
|
+
<template>
|
|
706
|
+
<button class="fdy-chip" type="button" @click="picker?.open()">Cost centre</button>
|
|
707
|
+
<FdyCfl ref="picker" dialog-only v-model="costCentre" :fetch-page="fetchPage"
|
|
708
|
+
:columns="columns" :display="d" :row-key="k" />
|
|
709
|
+
</template>
|
|
710
|
+
```
|
|
668
711
|
|
|
669
712
|
Blazor carries the same surface in `PascalCase` with three differences: multi-select is a second
|
|
670
713
|
pair (`Values` / `ValuesChanged`) rather than a widened single binding, the fetch delegate is
|
|
@@ -911,7 +954,9 @@ button chrome). `data-fdy-mask="####-####"` formats while typing: `#` digit, `A`
|
|
|
911
954
|
alphanumeric, anything else is a literal. Needs `freeday-mask.js`.
|
|
912
955
|
|
|
913
956
|
## Form grid — `.fdy-form-grid`
|
|
914
|
-
Responsive two-column document header. Children are `.fdy-field`; `.fdy-field--full` spans both
|
|
957
|
+
Responsive two-column document header. Children are `.fdy-field`; `.fdy-field--full` spans both
|
|
958
|
+
columns. For a form that must stay **one** column, do not reach for this grid — it pairs fields up
|
|
959
|
+
as soon as the container is wide enough. Use a flex column and mark the fields `--full`.
|
|
915
960
|
|
|
916
961
|
## Filter bar — `.fdy-filterbar`
|
|
917
962
|
A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · default · `--w-lg` ·
|
|
@@ -1451,11 +1496,17 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
|
|
|
1451
1496
|
## Nav (menu) — `.fdy-nav`
|
|
1452
1497
|
Navigation links, **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
|
|
1453
1498
|
`__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
|
|
1454
|
-
|
|
1499
|
+
Nested groups are native `<details>`:
|
|
1455
1500
|
|
|
1456
1501
|
- `.fdy-nav__tree` + `<summary class="fdy-nav__item">` + `.fdy-nav__caret` → children in
|
|
1457
1502
|
`.fdy-nav__sub`
|
|
1458
1503
|
- `.fdy-nav__group` + `<summary class="fdy-nav__grouplabel">` → a collapsible section
|
|
1504
|
+
- **`--flat` on the `<nav>` is the static variant**: group labels that never collapse. It drops the
|
|
1505
|
+
disclosure caret, returns the label to `cursor: default`, and removes the divider between groups,
|
|
1506
|
+
so `.fdy-nav__grouplabel` can be a plain `<p>`/`<div>` heading ("OVERVIEW", "ADMINISTRATION")
|
|
1507
|
+
outside any `<details>`. Reach for it instead of undoing those three rules per app — which is what
|
|
1508
|
+
two consoles did for four months, because this line used to claim `--flat` "drops the surface",
|
|
1509
|
+
and it never did: `.fdy-nav` paints no surface to drop.
|
|
1459
1510
|
- **`--horizontal`** lays the same links out as a row, for a **top-nav application** (primary
|
|
1460
1511
|
navigation in `.fdy-appbar` or `.fdy-app__topbar`, no sidebar). Same item, same states, same
|
|
1461
1512
|
`aria-current="page"`; the row scrolls if it runs out of width. On `.fdy-appbar--primary` the
|
|
@@ -1496,6 +1547,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
|
1496
1547
|
`.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
|
|
1497
1548
|
so the same look serves routed sub-navigation built from plain links. See the note under Nav.
|
|
1498
1549
|
|
|
1550
|
+
> **The strip's line is an inset `box-shadow`, not a `border-bottom`** (`#052`; through 3.1.0 it
|
|
1551
|
+
> was a border, so an app upgrading past that release changes the override with it). To
|
|
1552
|
+
> remove it — a strip inside a header that draws its own rule — override `box-shadow:none`;
|
|
1553
|
+
> `border-bottom:0` no longer reaches it. The swap is not cosmetic: `.fdy-tabs__list` scrolls
|
|
1554
|
+
> horizontally, a scroll container clips its descendants to the padding box, and the tab's
|
|
1555
|
+
> `margin-bottom:-1px` (which lifted its 2px underline onto that border) therefore pushed the
|
|
1556
|
+
> underline's lower half into the clip — the underline painted 1px, and the strip carried 1px of
|
|
1557
|
+
> vertical scroll that ate trackpad gestures. A shadow paints on the box, not on the scrolled
|
|
1558
|
+
> content, so the line needs no overhang to sit under. Guarded in `browser/tabs-strip.mjs`.
|
|
1559
|
+
|
|
1499
1560
|
```html
|
|
1500
1561
|
<div class="fdy-tabs" data-fdy-tabs>
|
|
1501
1562
|
<div class="fdy-tabs__list" role="tablist" aria-label="Invoice detail">
|
|
@@ -1521,7 +1582,7 @@ so the same look serves routed sub-navigation built from plain links. See the no
|
|
|
1521
1582
|
| `open` | `boolean` | Controlled: your state decides. The wrapper cancels the native `cancel` event so the DOM can never disagree with it. |
|
|
1522
1583
|
| `title` | `string` (Vue) · `ReactNode` | Names the dialog through `aria-labelledby`. Vue also offers a `title` slot when the heading needs markup. |
|
|
1523
1584
|
| `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
|
|
1524
|
-
| `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
|
|
1585
|
+
| `size?` | `'sm' \| 'md' \| 'lg' \| 'wide' \| 'cfl'` | Dialog width. `cfl` is the 46rem choose-from-list width; `FdyCfl` applies it itself, so pass it only for a dialog you build at that width. |
|
|
1525
1586
|
| `dismissible?` | `boolean` | Whether Escape, the backdrop and the × can close it. Off means the reader has to take a footer action, which is the point of a blocking confirm. |
|
|
1526
1587
|
| `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
|
|
1527
1588
|
| `footer?` | `ReactNode` | React. Vue: the `footer` slot. |
|
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.3.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.3.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`.
|
package/USAGE.md
CHANGED
|
@@ -23,10 +23,20 @@ the single biggest cause of "flat grey mush". Pick the role, not the size:
|
|
|
23
23
|
| Card / row title | `.fdy-title-card` (or `.fdy-card__title`) | A title inside a card or list row (`<h3>`). |
|
|
24
24
|
| Body | *(default)* | Running text. |
|
|
25
25
|
| Muted / caption | `.fdy-text-muted`, `.fdy-text-caption` | Secondary text, timestamps, help. |
|
|
26
|
+
| Subtle | `.fdy-text-subtle` | The faintest ink. Placeholders and decoration — chevrons, separators, out-of-month days. **Read the contract below before using it for a sentence.** |
|
|
26
27
|
|
|
27
28
|
Everything else is body. If you reach for a fourth title size, you probably need a section, not a
|
|
28
29
|
font size.
|
|
29
30
|
|
|
31
|
+
**`--color-text-subtle` is not a third body-text tier, and dark is where that bites.** It clears AA
|
|
32
|
+
on `--color-surface` in both themes (light 4.69:1, dark 4.88:1) — which is what `.fdy-input` and
|
|
33
|
+
`.fdy-textarea` paint, so a placeholder is safe. It clears AA on **nothing else**, in either theme:
|
|
34
|
+
`--color-surface-3` reads 4.11:1 in light and 3.66:1 in dark. Nor will it ever: reaching 4.5 there
|
|
35
|
+
needs a tone within a hair of `--color-text-muted`, and a subtle that equals muted is not a tier. So a small uppercase
|
|
36
|
+
label, a card's second line, or anything a reader must actually read on a raised or tinted surface
|
|
37
|
+
takes `--color-text-muted`. `test/contrast.test.mjs` holds both halves of that split, which is why
|
|
38
|
+
the ramp cannot quietly drift into either mistake.
|
|
39
|
+
|
|
30
40
|
|
|
31
41
|
## Borders: two roles, two contrast contracts
|
|
32
42
|
|
|
@@ -7,27 +7,33 @@
|
|
|
7
7
|
dialog are rendered and re-rendered by Blazor, rows come from an async LoadPage callback, and
|
|
8
8
|
the <dialog> is opened/closed through the same freeday-blazor.js interop FdyModal uses. *@
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
10
|
+
@* The field is optional (#054): with DialogOnly the component renders the dialog alone, for a
|
|
11
|
+
caller whose trigger is already its own — a chip, a table cell, a menu item — and opens it
|
|
12
|
+
through @ref: `await picker.OpenAsync()`. The host generates no box. *@
|
|
13
|
+
@if (!DialogOnly)
|
|
14
|
+
{
|
|
15
|
+
<div class="@FieldGroupClass">
|
|
16
|
+
<input class="fdy-input" type="text" readonly value="@DisplayValue"
|
|
17
|
+
placeholder="@Placeholder" aria-labelledby="@AriaLabelledby"
|
|
18
|
+
aria-invalid="@(Invalid ? "true" : null)" aria-readonly="@(Readonly ? "true" : null)" />
|
|
19
|
+
@if (Clearable && Value is not null && !Disabled && !Readonly)
|
|
20
|
+
{
|
|
21
|
+
<button class="fdy-input-group__btn" type="button" @onclick="ClearAsync" aria-label="@ClearLabel">
|
|
22
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
|
23
|
+
stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
24
|
+
<path d="M6 6l12 12M18 6L6 18"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
}
|
|
28
|
+
<button class="fdy-input-group__btn" type="button" @onclick="OpenAsync"
|
|
29
|
+
disabled="@(Disabled || Readonly)" aria-label="@OpenLabel">
|
|
17
30
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
|
18
|
-
stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
19
|
-
<
|
|
31
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
32
|
+
<circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path>
|
|
20
33
|
</svg>
|
|
21
34
|
</button>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
disabled="@(Disabled || Readonly)" aria-label="@OpenLabel">
|
|
25
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
|
26
|
-
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
27
|
-
<circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path>
|
|
28
|
-
</svg>
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
35
|
+
</div>
|
|
36
|
+
}
|
|
31
37
|
|
|
32
38
|
<dialog @ref="Root" class="fdy-modal fdy-modal--cfl" aria-labelledby="@_titleId">
|
|
33
39
|
<div class="fdy-modal__header">
|
|
@@ -70,6 +70,13 @@ public partial class FdyCfl<TRow>
|
|
|
70
70
|
/// <summary>Debounce (ms) between a keystroke and the search request.</summary>
|
|
71
71
|
[Parameter] public int SearchDebounceMs { get; set; } = 250;
|
|
72
72
|
|
|
73
|
+
/// <summary>Render the dialog and NO field, for a caller whose trigger is already its own — a chip,
|
|
74
|
+
/// a table cell, a menu item — opened through <c>@ref</c>: <c>await picker.OpenAsync()</c>. The raw
|
|
75
|
+
/// path has had exactly this since the enhancer's <c>[data-fdy-cfl-open]</c>. <c>Placeholder</c>,
|
|
76
|
+
/// <c>Clearable</c> and the field's aria parameters have nothing to name here and are ignored;
|
|
77
|
+
/// <c>Disabled</c> and <c>Readonly</c> still refuse to open.</summary>
|
|
78
|
+
[Parameter] public bool DialogOnly { get; set; }
|
|
79
|
+
|
|
73
80
|
private readonly string _titleId = $"fdy-cfl-{Guid.NewGuid():N}-title";
|
|
74
81
|
private int _dialogToken;
|
|
75
82
|
private bool _open;
|
|
@@ -93,7 +100,10 @@ public partial class FdyCfl<TRow>
|
|
|
93
100
|
protected override async ValueTask HydrateAsync()
|
|
94
101
|
=> _dialogToken = await JS.InvokeAsync<int>("FreedayBlazor.dialogInit", Root, Self, nameof(OnDismiss), true);
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
/// <summary>Open the picker. Public because with <see cref="DialogOnly"/> there is no field to
|
|
104
|
+
/// open it, and because a caller with its own trigger is the case that made #054: it guards
|
|
105
|
+
/// exactly as the built trigger does, so this cannot bypass Disabled/Readonly.</summary>
|
|
106
|
+
public async Task OpenAsync()
|
|
97
107
|
{
|
|
98
108
|
if (Disabled || Readonly || _open) return;
|
|
99
109
|
_open = true;
|
|
@@ -105,7 +115,8 @@ public partial class FdyCfl<TRow>
|
|
|
105
115
|
await LoadAsync(reset: true);
|
|
106
116
|
}
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
/// <summary>Close the picker without committing, the same path Esc and the footer button take.</summary>
|
|
119
|
+
public async Task CloseAsync()
|
|
109
120
|
{
|
|
110
121
|
if (!_open) return;
|
|
111
122
|
_open = false;
|
|
@@ -19,7 +19,7 @@ public partial class FdyModal
|
|
|
19
19
|
/// <summary>Optional footer (action buttons). Omitted entirely when null.</summary>
|
|
20
20
|
[Parameter] public RenderFragment? FooterContent { get; set; }
|
|
21
21
|
|
|
22
|
-
/// <summary>Size modifier: <c>sm</c> | <c>md</c> | <c>lg</c> | <c>wide</c
|
|
22
|
+
/// <summary>Size modifier: <c>sm</c> | <c>md</c> | <c>lg</c> | <c>wide</c> | <c>cfl</c> (the 46rem choose-from-list width).</summary>
|
|
23
23
|
[Parameter] public string? Size { get; set; }
|
|
24
24
|
|
|
25
25
|
/// <summary>When true (default), Esc and a backdrop click dismiss the dialog and a close
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
import { useEffect, useId, useRef, useState } from 'react';
|
|
1
|
+
import type { JSX, Ref } from 'react';
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
// React port of adapters/vue/components/FdyCfl.vue over freeday's `.fdy-cfl*` +
|
|
5
5
|
// `.fdy-input-group` classes (see src/components/cfl.css, input-group.css). A controlled
|
|
@@ -71,9 +71,27 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
|
|
|
71
71
|
describedby?: string;
|
|
72
72
|
id?: string;
|
|
73
73
|
ariaLabelledby?: string;
|
|
74
|
+
/** Render the dialog and NO field, for a caller whose trigger is already its own — a chip, a table
|
|
75
|
+
* cell, a menu item — opened through the ref: `picker.current?.open()`. The raw path has had
|
|
76
|
+
* exactly this since the enhancer's `[data-fdy-cfl-open]`; the typed wrappers were a field *plus*
|
|
77
|
+
* a dialog with no way in, so a screen that could not spend a 22rem readonly input per trigger had
|
|
78
|
+
* to hand-roll the picker (#054). The host generates no box, so it can sit anywhere.
|
|
79
|
+
* `placeholder`, `clearable`, `id` and the field's aria props have nothing to name here and are
|
|
80
|
+
* ignored; `disabled` and `readonly` still refuse to open. */
|
|
81
|
+
dialogOnly?: boolean;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
/** What a `ref` on `<FdyCfl>` gives you. Both guard exactly as the trigger does, so a programmatic
|
|
85
|
+
* open cannot bypass `disabled`/`readonly`. */
|
|
86
|
+
export interface FdyCflHandle {
|
|
87
|
+
open: () => void;
|
|
88
|
+
close: () => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function FdyCflInner<Row extends Record<string, unknown>>(
|
|
92
|
+
props: FdyCflProps<Row>,
|
|
93
|
+
ref: Ref<FdyCflHandle>,
|
|
94
|
+
): JSX.Element {
|
|
77
95
|
const baseId: string = useId();
|
|
78
96
|
const fieldId: string = props.id ?? `${baseId}-field`;
|
|
79
97
|
const titleId: string = `${baseId}-title`;
|
|
@@ -301,6 +319,10 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
301
319
|
dialogRef.current?.close();
|
|
302
320
|
}
|
|
303
321
|
|
|
322
|
+
/* No dependency array on purpose: both close over state (the ticks to seed, whether the field is
|
|
323
|
+
disabled), so a handle frozen at first render would open a picker with last week's props. */
|
|
324
|
+
useImperativeHandle(ref, (): FdyCflHandle => ({ open: openDialog, close: closeDialog }));
|
|
325
|
+
|
|
304
326
|
// Fires for every close path (button, Esc, commit); centralise cleanup here.
|
|
305
327
|
function onClose(): void {
|
|
306
328
|
reqIdRef.current++; // invalidate any in-flight fetch so it can't apply after close
|
|
@@ -330,46 +352,50 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
330
352
|
}, []);
|
|
331
353
|
|
|
332
354
|
return (
|
|
333
|
-
<div className=
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
355
|
+
<div className={props.dialogOnly === true ? 'fdy-cfl__host' : 'fdy-input-group'}>
|
|
356
|
+
{props.dialogOnly === true ? null : (
|
|
357
|
+
<>
|
|
358
|
+
<input
|
|
359
|
+
id={fieldId}
|
|
360
|
+
className="fdy-input"
|
|
361
|
+
type="text"
|
|
362
|
+
readOnly
|
|
363
|
+
value={displayValue}
|
|
364
|
+
placeholder={props.placeholder}
|
|
365
|
+
aria-labelledby={props.ariaLabelledby}
|
|
366
|
+
aria-invalid={isInvalid ? 'true' : undefined}
|
|
367
|
+
aria-describedby={props.describedby}
|
|
368
|
+
disabled={isDisabled}
|
|
369
|
+
/>
|
|
370
|
+
{showClear ? (
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
className="fdy-input-group__btn"
|
|
374
|
+
aria-label={clearLabelText}
|
|
375
|
+
onClick={clearValue}
|
|
376
|
+
>
|
|
377
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
378
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
379
|
+
</svg>
|
|
380
|
+
</button>
|
|
381
|
+
) : null}
|
|
382
|
+
<button
|
|
383
|
+
ref={triggerRef}
|
|
384
|
+
type="button"
|
|
385
|
+
className="fdy-input-group__btn"
|
|
386
|
+
aria-haspopup="dialog"
|
|
387
|
+
aria-labelledby={props.ariaLabelledby}
|
|
388
|
+
aria-label={props.ariaLabelledby ? undefined : (props.openLabel ?? 'Open search')}
|
|
389
|
+
disabled={isDisabled || isReadonly}
|
|
390
|
+
onClick={openDialog}
|
|
391
|
+
>
|
|
392
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
393
|
+
<circle cx={11} cy={11} r={7}></circle>
|
|
394
|
+
<path d="m21 21-4.35-4.35"></path>
|
|
395
|
+
</svg>
|
|
396
|
+
</button>
|
|
397
|
+
</>
|
|
398
|
+
)}
|
|
373
399
|
|
|
374
400
|
<dialog ref={dialogRef} className="fdy-modal fdy-modal--cfl" aria-labelledby={titleId} onClose={onClose} onKeyDown={onKeydown}>
|
|
375
401
|
<div className="fdy-modal__header">
|
|
@@ -379,7 +405,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
379
405
|
|
|
380
406
|
<div className="fdy-modal__body">
|
|
381
407
|
<div className="fdy-cfl__search">
|
|
382
|
-
<div className="fdy-input-group"
|
|
408
|
+
<div className="fdy-input-group">
|
|
383
409
|
<span className="fdy-input-group__addon fdy-input-group__addon--icon">
|
|
384
410
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
385
411
|
<circle cx={11} cy={11} r={7}></circle>
|
|
@@ -478,3 +504,12 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
|
|
|
478
504
|
</div>
|
|
479
505
|
);
|
|
480
506
|
}
|
|
507
|
+
|
|
508
|
+
/* `forwardRef` types its argument as a plain function, which erases `Row`: every consumer would get
|
|
509
|
+
`FdyCflProps<Record<string, unknown>>` and lose the row type the whole component exists to carry.
|
|
510
|
+
The cast restores the generic call signature, and it is the standard shape for a generic component
|
|
511
|
+
with a ref while `react` is `>=18` (React 19's ref-as-prop would not need it, and the peer range
|
|
512
|
+
still admits 18). */
|
|
513
|
+
export const FdyCfl = forwardRef(FdyCflInner) as <Row extends Record<string, unknown>>(
|
|
514
|
+
props: FdyCflProps<Row> & { ref?: Ref<FdyCflHandle> },
|
|
515
|
+
) => JSX.Element;
|
|
@@ -13,7 +13,8 @@ export interface FdyModalProps {
|
|
|
13
13
|
open: boolean;
|
|
14
14
|
title: ReactNode;
|
|
15
15
|
onClose: () => void;
|
|
16
|
-
|
|
16
|
+
/* 'cfl' is the choose-from-list width (46rem); see the Vue wrapper for why it was missing. */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg' | 'wide' | 'cfl';
|
|
17
18
|
dismissible?: boolean;
|
|
18
19
|
/** aria-label for the × button. Default 'Close'. */
|
|
19
20
|
closeLabel?: string;
|
|
@@ -33,7 +33,7 @@ export { FdyDatepicker, type FdyDatepickerProps } from './components/FdyDatepick
|
|
|
33
33
|
export { FdyDateRange, type FdyDateRangeProps, type DateRangeValue } from './components/FdyDateRange';
|
|
34
34
|
export { FdyAutocomplete, type FdyAutocompleteProps } from './components/FdyAutocomplete';
|
|
35
35
|
export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components/FdyCascade';
|
|
36
|
-
export { FdyCfl, type FdyCflProps, type CflColumn, type CflPage } from './components/FdyCfl';
|
|
36
|
+
export { FdyCfl, type FdyCflProps, type FdyCflHandle, type CflColumn, type CflPage } from './components/FdyCfl';
|
|
37
37
|
/** Narrow `FdyCfl`'s `Row | Row[] | null` to the `Row | null` a single-select can produce; throws
|
|
38
38
|
* on the array only a `multiple` field emits. */
|
|
39
39
|
export { singleRow } from '../core/cfl-value';
|