@cahyo-dimas/freeday 3.0.0 → 3.2.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 +144 -0
- package/COMPONENTS.md +46 -3
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/USAGE.md +10 -0
- package/adapters/blazor/FdyModal.razor.cs +1 -1
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/react/components/FdyModal.tsx +2 -1
- package/adapters/react/components/FdyTableFooter.tsx +1 -1
- package/adapters/vue/components/FdyModal.vue +4 -1
- package/adapters/vue/components/FdyTableFooter.vue +1 -1
- package/dist/freeday-table.js +7 -0
- package/dist/freeday.bundle.css +115 -13
- package/dist/freeday.css +112 -10
- package/dist/freeday.js +7 -0
- package/dist/freeday.tokens.css +2 -2
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/components/busy.css +10 -1
- package/src/components/cfl.css +57 -2
- package/src/components/composition.css +15 -2
- package/src/components/filterbar.css +3 -2
- package/src/components/input.css +9 -0
- package/src/components/tabs.css +18 -3
- package/tokens/tokens.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,150 @@ 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.2.0] - 2026-08-28
|
|
13
|
+
|
|
14
|
+
**Dua cacat yang tak bisa di-screenshot, dan satu komponen yang akhirnya muat di panel 420px.**
|
|
15
|
+
Ketiganya datang dari layar sungguhan. Yang menyatukannya: tak satu pun terlihat di DOM atau di
|
|
16
|
+
stylesheet — yang satu hanya bergerak, yang satu hanya terbaca, yang satu hanya muat atau tidak.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Strip tab tak lagi menggulung vertikal, dan garis aktifnya kini benar-benar 2px** (`#052`, dari
|
|
20
|
+
`IDU_AI_DOC_SAPB1_CLIENT`). Dilaporkan pengguna yang bisa merasakannya tapi tak bisa memotretnya:
|
|
21
|
+
*"panel tab ini menggulung vertikal, susah di-screenshot."* Memang begitu — **1px**.
|
|
22
|
+
`overflow-x:auto` tak bisa meminta satu sumbu saja (CSS Overflow §3: sumbu lain yang `visible`
|
|
23
|
+
ikut jadi `auto`), jadi 1px yang digantungkan `margin-bottom:-1px` milik tab — yang ada justru
|
|
24
|
+
untuk mengangkat underline 2px-nya ke atas garis strip — berubah jadi scrollport sungguhan yang
|
|
25
|
+
memakan gestur trackpad. Yang tak dilaporkan siapa pun, dan hanya piksel yang bisa membedakannya:
|
|
26
|
+
scroll container **memotong** keturunannya di padding box, jadi separuh bawah underline itu ikut
|
|
27
|
+
terpotong. Underline aktif terkirim **1px** selama negative margin itu ada — CSS-nya bilang 2px,
|
|
28
|
+
DOM-nya bilang 2px, layarnya bilang 1. Garis strip kini `box-shadow` inset (dicat di kotaknya,
|
|
29
|
+
bukan di konten yang menggulung), dan tab tak lagi menggantung. **Bagi konsumen yang menyembunyikan
|
|
30
|
+
garis itu:** `border-bottom:0` tak lagi menjangkaunya, pakai `box-shadow:none`. Sapuan ke seluruh
|
|
31
|
+
scroller lain (`.fdy-table-scroll`, `.fdy-nav--horizontal`, `.fdy-carousel__viewport`) bersih —
|
|
32
|
+
`.fdy-tabs__list` satu-satunya.
|
|
33
|
+
- **`--color-text-subtle` gelap naik ke `slate-450`** (`#053` §2, dari
|
|
34
|
+
`IDU_AI_DOC_SAPB1_ADDON_DESKTOP_APP`). 4.02:1 di `--color-surface` gelap, di bawah AA, dan yang
|
|
35
|
+
memakainya bukan cuma app: **placeholder kit sendiri** (`.fdy-datepicker__value--placeholder` dan
|
|
36
|
+
sebelas `.fdy-cal__day.is-outside` dalam satu kalender). Yang menarik, tokennya tak melanggar
|
|
37
|
+
kontraknya — gerbang kontras memang menguji tier ini di **3:1 sebagai non-teks**; yang salah
|
|
38
|
+
adalah kit memakai tier dekoratif untuk teks yang berutang AA. Sekarang **4.88:1** di
|
|
39
|
+
`--color-surface`, yaitu permukaan yang dicat setiap `.fdy-input`, jadi placeholder lolos di tempat
|
|
40
|
+
placeholder benar-benar duduk. Light tak disentuh (sudah 4.69). **AA di ketiga permukaan gelap
|
|
41
|
+
sengaja tidak dikejar**: mencapainya butuh nada sejengkal dari `slate-400`, yang *adalah*
|
|
42
|
+
`--color-text-muted`, dan subtle yang sama dengan muted bukan tier lagi.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`FdyCfl` punya layout sempit, tanpa satu pun prop baru** (`#053` §1). Di bawah **30rem**,
|
|
46
|
+
barisnya menata diri sebagai list item: kolom pertama jadi judul, sisanya satu baris detail di
|
|
47
|
+
bawahnya, `<thead>` dilepas. **Container query, bukan media query** — dialog bukan viewport, dan
|
|
48
|
+
komponen yang sama adalah tabel di halaman desktop sekaligus daftar di dalam panel 420px yang
|
|
49
|
+
ditambatkan di SAP Business One. Kontrak datanya tak berubah: `columns` sudah menyebut kolom mana
|
|
50
|
+
yang identitas. Keempat stack dapat sekaligus, karena yang bekerja CSS.
|
|
51
|
+
- **`size="cfl"` di `FdyModal`** (`#053` §4), Vue · React · Blazor. `.fdy-modal--cfl` (46rem) sudah
|
|
52
|
+
ada di CSS sejak lama tapi tak terjangkau dari adapter mana pun; `FdyCfl` memasang kelasnya
|
|
53
|
+
sendiri, jadi celah ini hanya muncul bagi yang membangun dialog sendiri selebar itu. **Bukan
|
|
54
|
+
breaking** meski union publiknya melebar: kebijakan 2.1.0 menyasar tipe di posisi *keluaran*
|
|
55
|
+
(handler yang menerima nilai lebih lebar); ini masukan, dan pemanggil yang mengirim nilai lama
|
|
56
|
+
tetap valid.
|
|
57
|
+
|
|
58
|
+
### Added: guards
|
|
59
|
+
- `browser/tabs-strip.mjs` — nol overflow vertikal, underline 2px terbaca dari **piksel** yang
|
|
60
|
+
benar-benar dicat, dan gulir horizontal yang tetap hidup. Diverifikasi mutasi: dengan aturan 3.1.0
|
|
61
|
+
dikembalikan, 2 dari 3 merah.
|
|
62
|
+
- `browser/cfl-narrow.mjs` — menjalankan `FdyCfl.vue` sungguhan di viewport **420px**, lebar
|
|
63
|
+
konsumen yang melaporkannya, karena container query menanyakan lebar *dialog* dan hanya dialog
|
|
64
|
+
sungguhan yang punya lebar.
|
|
65
|
+
- `test/contrast.test.mjs` dapat pasangan baru khusus placeholder, terpisah dari tier dekoratifnya,
|
|
66
|
+
jadi kedua paruh keputusan itu dijaga: gagal di 4.02 kalau tokennya dikembalikan, dan tetap 3:1
|
|
67
|
+
untuk chevron/separator.
|
|
68
|
+
|
|
69
|
+
### Docs
|
|
70
|
+
- `COMPONENTS.md`: kontrak override garis strip tab, layout sempit `FdyCfl`, dan `size="cfl"`.
|
|
71
|
+
- `USAGE.md`: `.fdy-text-subtle` akhirnya masuk tabel peran teks — tabel itu berhenti di "muted",
|
|
72
|
+
padahal kelasnya publik — beserta kontrak kontrasnya di kedua tema.
|
|
73
|
+
|
|
74
|
+
### Tidak diperbaiki, dan alasannya
|
|
75
|
+
- **`--color-text-muted` 3.94:1 di `--color-surface-raised`** (`#053` §3) **tidak reproduksi.** Di
|
|
76
|
+
`soft`, `--color-surface-raised` *identik* dengan `--color-surface` (7.28:1); di `glass`,
|
|
77
|
+
komposit terburuknya 6.70:1; dan muted tak pernah turun di bawah **5.46:1** di mana pun dalam ramp
|
|
78
|
+
gelap. Angka 3.94 itu sendiri nyata: ia persis `slate-400` di atas `slate-700`, yaitu
|
|
79
|
+
`--color-border-strong` — token border yang tak dipakai komponen mana pun sebagai latar. Item ini
|
|
80
|
+
dibiarkan **open** dengan pertanyaan balik ke pelapor, bukan ditutup sebagai withdrawn.
|
|
81
|
+
|
|
82
|
+
## [3.1.0] - 2026-08-27
|
|
83
|
+
|
|
84
|
+
**Dua cacat lebar yang dilaporkan dari layar sungguhan, dan keduanya tak kelihatan di DOM.** Markup-nya
|
|
85
|
+
benar, class-nya benar, kotaknya cuma berhenti lebih awal. Rilis ini juga membuat satu kalimat
|
|
86
|
+
`COMPONENTS.md` jadi benar setelah lama tidak.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
- **Kontrol di dalam `.fdy-field` kini selebar field-nya** (`improvement-notes` #051, dari
|
|
90
|
+
`IDU_EMATE_APPL_WEB` dan `IDU_EMATE_ACCT_WEB`). `.fdy-input-group`, `.fdy-combo`,
|
|
91
|
+
`.fdy-autocomplete` dan `.fdy-cascade` masing-masing di-cap `22rem` — cap untuk kontrol yang
|
|
92
|
+
berdiri sendiri, tanpa field yang bisa memberinya lebar — dan cap itu hanya dilepas **di satu
|
|
93
|
+
container**, `.fdy-filterbar`, yang menyebut dua dari empatnya. Akibatnya baris `--full` di
|
|
94
|
+
`.fdy-form-grid` memegang combo 352px di bawah label 600px, sementara `.fdy-input`
|
|
95
|
+
di sebelahnya (yang tak pernah punya cap) mengisi penuh: satu form terlihat tak sejajar dengan
|
|
96
|
+
dirinya sendiri. Yang paling mahal justru versi yang dilaporkan pengguna tanpa diminta — *"jarak
|
|
97
|
+
search ke filter status agak jauh, ya?"* — pada toolbar dengan field `26rem`: kontrolnya 352px,
|
|
98
|
+
jadi **64px ruang mati ada DI DALAM field**, dan `gap:var(--space-3)` yang ditulis penulisnya
|
|
99
|
+
terbaca 76px di layar. Tak ada satu pun langkah debug yang wajar bisa melihatnya: gap-nya benar,
|
|
100
|
+
flex-nya benar.
|
|
101
|
+
Perbaikannya satu aturan di `input.css`, bukan selector ketiga di samping filterbar, dan bentuknya
|
|
102
|
+
**sudah ada di kit sejak #017** — `.fdy-field>.fdy-datepicker` melepas cap picker dengan komentar
|
|
103
|
+
"a picker fills the field *like every other control*". Kalimat itu tidak benar untuk empat kontrol
|
|
104
|
+
di atas, dan sekarang benar. Cap `22rem` tetap ada **di `.fdy-field`**, jadi field yang tak
|
|
105
|
+
dilebarkan siapa pun tak berubah sedikit pun.
|
|
106
|
+
Ikut terangkat, dan tak ada di laporan mana pun: `.fdy-autocomplete` dan `.fdy-cascade` di field
|
|
107
|
+
`--w-2xl` (25rem) berhenti 48px lebih pendek **di dalam filterbar sendiri** — container yang kit
|
|
108
|
+
kira sudah beres. Aturan lama di `filterbar.css` dihapus, bukan dibiarkan di samping yang baru:
|
|
109
|
+
dua aturan yang mengatakan hal sama persis adalah cara filterbar jadi tahu sesuatu yang tak
|
|
110
|
+
diketahui `input.css`.
|
|
111
|
+
- **`.fdy-pagination` akhirnya benar-benar dikirim** (#050 §2). `COMPONENTS.md` menyuruh konsumen
|
|
112
|
+
menulis `<nav class="fdy-pagination" data-fdy-table-pagination>`, dan class blok itu **nol match di
|
|
113
|
+
seluruh paket** — bukan di enhancer, bukan di tiga footer typed. Tak ada yang tampak berbeda,
|
|
114
|
+
karena blok itu memang tak punya rule (`NEXT-UP` #9), tapi selector konsumen atau asersi e2e pada
|
|
115
|
+
blok itu **lulus terhadap markup tulisan tangan dan gagal terhadap keluaran kit sendiri**, padahal
|
|
116
|
+
kedua jalur seharusnya bisa saling menggantikan. Enhancer kini menyetelnya sekali saat init;
|
|
117
|
+
`FdyTableFooter` Vue/React/Blazor merendernya.
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
- **`.fdy-stats--inline`** — strip KPI yang **memeluk angkanya** alih-alih membentang, untuk yang
|
|
121
|
+
duduk di samping judul halaman (#050 §1). Ini jawaban atas cacat yang tak bisa diperbaiki dengan
|
|
122
|
+
dokumentasi saja: `.fdy-stat` membawa `container-type:inline-size` (itu yang memberi `__value`
|
|
123
|
+
tipe fluidnya), dan **inline-size containment berarti tile tak menyumbang lebar intrinsik sama
|
|
124
|
+
sekali**. Jadi floor `11rem` bawaan bukan pilihan rasa — ia satu-satunya yang memberi track itu
|
|
125
|
+
ukuran — dan konsumen yang me-retrack grid ke konten mendapat tiga track nol: tiga label saling
|
|
126
|
+
menimpa (`COMPAMEMBERSTA…`) dan halaman meluber ke samping. Terukur dari header
|
|
127
|
+
`IDU_EMATE_ACCT_WEB`: strip 40px, seluruhnya gap. `justify-items` lebih tajam lagi — apa pun
|
|
128
|
+
selain `stretch` mengecilkan tile ke sumbangan nol itu **walau track-nya sudah benar lebarnya**.
|
|
129
|
+
`--inline` melepas container-nya, jadi tile-nya berukuran sesuai kontennya sendiri dan `__value`
|
|
130
|
+
memakai `--text-3xl` datar: track yang seukuran kontennya tak pernah jadi track sempit yang
|
|
131
|
+
membuat tipe fluid itu ada. Konsumen tak perlu lagi menebak floor sendiri.
|
|
132
|
+
|
|
133
|
+
### Docs
|
|
134
|
+
- **`COMPONENTS.md` §Stats akhirnya menyebut containment-nya.** `container-type`, `cqw`,
|
|
135
|
+
`inline-size` dan `contain` nol match di seluruh berkas itu sebelum rilis ini, padahal detail
|
|
136
|
+
implementasi tipe `__value` itu bocor langsung ke kontrak layout siapa pun yang me-retrack grid-nya.
|
|
137
|
+
Kini tertulis sebagai batasan: override `grid-template-columns` wajib punya floor eksplisit, dan
|
|
138
|
+
`justify-items` harus tetap `stretch`.
|
|
139
|
+
- **§Field menyebut cap-nya.** `22rem` dan `max-width` juga nol match di `COMPONENTS.md` sebelum ini,
|
|
140
|
+
jadi cap maupun pengecualian filterbar-nya hanya bisa ditemukan dengan mengukur form yang sudah
|
|
141
|
+
dirender. Aturannya sekarang satu kalimat: **beri ukuran pada field, jangan pada kontrolnya.**
|
|
142
|
+
|
|
143
|
+
### Guarded
|
|
144
|
+
- `test/css.test.mjs`: bentuk CSS-nya — keempat kontrol dilepas di `.fdy-field`, `.fdy-field`
|
|
145
|
+
mempertahankan cap-nya sendiri, aturan filterbar lama tak boleh kembali, dan `--inline` harus
|
|
146
|
+
keluar dari container maupun clamp `cqw`-nya.
|
|
147
|
+
- `browser/field-width.mjs` (6 test, Chrome sungguhan): hasilnya — keduanya adalah **outcome layout**
|
|
148
|
+
yang tak terlihat di stylesheet. Termasuk asersi bahwa gap yang terlihat pembaca sama dengan gap
|
|
149
|
+
yang ditulis penulisnya, dan tile `--inline` diukur terhadap referensi yang berukuran-konten
|
|
150
|
+
secara konstruksi, bukan sekadar "bukan nol": tanpa itu, satu track yang masih memegang floor
|
|
151
|
+
`11rem` lolos karena strip-nya tetap masuk akal.
|
|
152
|
+
- `test/docs.test.mjs`: berkas mana pun yang merender `__element` sebuah blok harus menyebut bloknya.
|
|
153
|
+
Dicari **secara struktur**, bukan dari daftar empat berkas yang salah — renderer pager kelima
|
|
154
|
+
adalah yang tak akan diingat siapa pun.
|
|
155
|
+
|
|
12
156
|
## [3.0.0] - 2026-08-26
|
|
13
157
|
|
|
14
158
|
**Kenapa MAJOR padahal rilis ini seluruhnya aditif.** Tak ada satu pun perubahan di bawah yang
|
package/COMPONENTS.md
CHANGED
|
@@ -227,7 +227,22 @@ carry the doctrine in markup.
|
|
|
227
227
|
sits half a label-height low in it, so labelled fields belong in `.fdy-filterbar`
|
|
228
228
|
(`align-items:flex-end`). In a toolbar, hide the label with `.fdy-visually-hidden`.
|
|
229
229
|
- `.fdy-stats` is a KPI grid of `.fdy-stat` (`__label` `__value` `__meta`). Deliberately **not**
|
|
230
|
-
cards. `.fdy-stats--boxed` for one shared surface with dividers
|
|
230
|
+
cards. `.fdy-stats--boxed` for one shared surface with dividers; `.fdy-stats--inline` for a strip
|
|
231
|
+
that **hugs its numbers** instead of spanning the row, which is what a page header wants.
|
|
232
|
+
- **A `.fdy-stat` in the default grid is size-contained, and that constrains what you may override.**
|
|
233
|
+
It carries `container-type:inline-size` — that is what gives `__value` its fluid type — and
|
|
234
|
+
inline-size containment means the tile contributes **no intrinsic width**. Two consequences, and
|
|
235
|
+
re-tracking the grid hits both at once:
|
|
236
|
+
- Content-based track sizing cannot work: `auto`, `max-content`, `min-content` and `minmax(0, …)`
|
|
237
|
+
all resolve to a **zero** track. The default `minmax(11rem, 1fr)` floor is not a style choice,
|
|
238
|
+
it is the only thing giving these tracks a size, so **any `grid-template-columns` override needs
|
|
239
|
+
an explicit floor of its own**.
|
|
240
|
+
- `justify-items` must stay `stretch`. A non-stretched grid item is sized to its (zero)
|
|
241
|
+
contribution, so `start` collapses the tile to nothing **even when the track is correctly wide**.
|
|
242
|
+
|
|
243
|
+
Use `--inline` rather than working around either: it drops the container, so its tiles size to
|
|
244
|
+
their own content, and its `__value` keeps the flat `--text-3xl`. A hugging track is never the
|
|
245
|
+
narrow track the fluid type exists for.
|
|
231
246
|
|
|
232
247
|
```html
|
|
233
248
|
<div class="fdy-page">
|
|
@@ -409,6 +424,13 @@ native control, otherwise a `<div>` + explicitly associated label.
|
|
|
409
424
|
|
|
410
425
|
- `.fdy-field` (+ `--full` inside `.fdy-form-grid`; widths `--w-sm` `--w-lg` `--w-xl` `--w-2xl`
|
|
411
426
|
`--w-grow` inside `.fdy-filterbar`)
|
|
427
|
+
- **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, and
|
|
429
|
+
so does any width you state yourself — and in every one of those cases the control (`.fdy-input`,
|
|
430
|
+
`.fdy-input-group`, `.fdy-combo`, `.fdy-autocomplete`, `.fdy-cascade`, and the pickers) spans the
|
|
431
|
+
field. So **size the field, never the control**: styling the control instead leaves the field
|
|
432
|
+
wider than what is in it, and that dead space is invisible in the DOM — it reads on screen as a
|
|
433
|
+
gap you did not write.
|
|
412
434
|
- `.fdy-label` (+`--required`) · `.fdy-input` (+`--error`) · `.fdy-textarea` · `.fdy-help` (+`--error`)
|
|
413
435
|
|
|
414
436
|
**Grouped controls are a `<fieldset>`, not a new block.** Put `.fdy-field` on the fieldset and
|
|
@@ -618,6 +640,15 @@ child `<ul>` is a branch, one without is a leaf. Needs `freeday-cascade.js`.
|
|
|
618
640
|
## Choose-from-list (CFL) — `data-fdy-cfl`
|
|
619
641
|
> **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).
|
|
620
642
|
|
|
643
|
+
> **Narrow: the results stack, and there is no prop for it** (`#053`). Under a `.fdy-cfl__results`
|
|
644
|
+
> narrower than **30rem** — a docked add-on panel, a phone — the rows lay out as list items instead
|
|
645
|
+
> of table cells: the first column becomes the title, the remaining columns become one detail line
|
|
646
|
+
> under it, and `<thead>` is dropped. It is a **container** query, so what decides is the dialog's
|
|
647
|
+
> width and not the viewport's; the same component is a table on a desktop page and a list inside a
|
|
648
|
+
> 420px panel, with no flag to pass and nothing to keep in sync. Your data contract does not change:
|
|
649
|
+
> `columns`, `display` and `rowKey` already say which column is the identity. If a screen genuinely
|
|
650
|
+
> needs columns at that width, override the `@container` block — do not widen the dialog.
|
|
651
|
+
|
|
621
652
|
### Props — `<FdyCfl>`
|
|
622
653
|
|
|
623
654
|
Four of these are required and carry the whole component: `fetchPage`, `columns`, `display`,
|
|
@@ -1216,7 +1247,9 @@ and breaks the moment the class changes.
|
|
|
1216
1247
|
## Pagination — `.fdy-pagination`
|
|
1217
1248
|
The block class on the `<nav>` is a **structural hook only**. It carries no rule of its own; the
|
|
1218
1249
|
`__list` / `__link` / `__ellipsis` elements do all the styling, and the data table targets
|
|
1219
|
-
`data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit
|
|
1250
|
+
`data-fdy-table-pagination`. Keep it on the wrapper anyway, for consistency with the rest of the kit
|
|
1251
|
+
— and the kit keeps it too: the enhancer sets it on the element it finds, and all three typed
|
|
1252
|
+
footers render it, so a selector or an e2e assertion on the block matches either path.
|
|
1220
1253
|
|
|
1221
1254
|
`<nav class="fdy-pagination" aria-label="Pagination">` → `<ul class="fdy-pagination__list">`;
|
|
1222
1255
|
each item is a `.fdy-pagination__link` (`<button>` when navigable, `<span aria-current="page">`
|
|
@@ -1472,6 +1505,16 @@ APG tabs: ←/→, Home/End, roving tabindex. Needs `freeday-tabs.js`.
|
|
|
1472
1505
|
`.fdy-tabs__tab` marks its active state from **`aria-selected="true"` or `aria-current="page"`**,
|
|
1473
1506
|
so the same look serves routed sub-navigation built from plain links. See the note under Nav.
|
|
1474
1507
|
|
|
1508
|
+
> **The strip's line is an inset `box-shadow`, not a `border-bottom`** (`#052`; through 3.1.0 it
|
|
1509
|
+
> was a border, so an app upgrading past that release changes the override with it). To
|
|
1510
|
+
> remove it — a strip inside a header that draws its own rule — override `box-shadow:none`;
|
|
1511
|
+
> `border-bottom:0` no longer reaches it. The swap is not cosmetic: `.fdy-tabs__list` scrolls
|
|
1512
|
+
> horizontally, a scroll container clips its descendants to the padding box, and the tab's
|
|
1513
|
+
> `margin-bottom:-1px` (which lifted its 2px underline onto that border) therefore pushed the
|
|
1514
|
+
> underline's lower half into the clip — the underline painted 1px, and the strip carried 1px of
|
|
1515
|
+
> vertical scroll that ate trackpad gestures. A shadow paints on the box, not on the scrolled
|
|
1516
|
+
> content, so the line needs no overhang to sit under. Guarded in `browser/tabs-strip.mjs`.
|
|
1517
|
+
|
|
1475
1518
|
```html
|
|
1476
1519
|
<div class="fdy-tabs" data-fdy-tabs>
|
|
1477
1520
|
<div class="fdy-tabs__list" role="tablist" aria-label="Invoice detail">
|
|
@@ -1497,7 +1540,7 @@ so the same look serves routed sub-navigation built from plain links. See the no
|
|
|
1497
1540
|
| `open` | `boolean` | Controlled: your state decides. The wrapper cancels the native `cancel` event so the DOM can never disagree with it. |
|
|
1498
1541
|
| `title` | `string` (Vue) · `ReactNode` | Names the dialog through `aria-labelledby`. Vue also offers a `title` slot when the heading needs markup. |
|
|
1499
1542
|
| `onClose` | `() => void` | React. Vue emits `close`. Fires from Escape, a backdrop click and the ×, all three of which exist only under `dismissible`. |
|
|
1500
|
-
| `size?` | `'sm' \| 'md' \| 'lg' \| 'wide'` | Dialog width. |
|
|
1543
|
+
| `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. |
|
|
1501
1544
|
| `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. |
|
|
1502
1545
|
| `closeLabel?` | `string` | Accessible name for the × button. Default `Close`. |
|
|
1503
1546
|
| `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.2.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.2.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
|
|
|
@@ -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
|
|
@@ -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;
|
|
@@ -75,7 +75,7 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
75
75
|
)}
|
|
76
76
|
|
|
77
77
|
{hasPager && (
|
|
78
|
-
<nav aria-label="Pagination">
|
|
78
|
+
<nav className="fdy-pagination" aria-label="Pagination">
|
|
79
79
|
<ul className="fdy-pagination__list">
|
|
80
80
|
<li>
|
|
81
81
|
<button type="button" className="fdy-pagination__link" aria-label="Previous page" disabled={currentPage1 === 1} onClick={(): void => goTo(currentPage1 - 1)}>‹</button>
|
|
@@ -16,7 +16,10 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
|
|
|
16
16
|
const props = withDefaults(defineProps<{
|
|
17
17
|
open: boolean;
|
|
18
18
|
title: string;
|
|
19
|
-
|
|
19
|
+
/* 'cfl' is the width the choose-from-list dialog uses (46rem). It was reachable from the CSS
|
|
20
|
+
and from nowhere else until #053: FdyCfl sets the class itself, so the gap only ever showed up
|
|
21
|
+
when an app built its own dialog at that width and had to hand-write the class. */
|
|
22
|
+
size?: 'sm' | 'md' | 'lg' | 'wide' | 'cfl';
|
|
20
23
|
dismissible?: boolean;
|
|
21
24
|
/** aria-label for the × button. Default 'Close'. Blazor has had `CloseLabel` since it
|
|
22
25
|
* shipped; these two hard-coded the string, so a non-English app could not rename it. */
|
package/dist/freeday-table.js
CHANGED
|
@@ -84,6 +84,13 @@
|
|
|
84
84
|
var countEl = root.querySelector('[data-fdy-table-count]');
|
|
85
85
|
var infoEl = root.querySelector('[data-fdy-table-info]');
|
|
86
86
|
var pagerEl = root.querySelector('[data-fdy-table-pagination]');
|
|
87
|
+
/* COMPONENTS.md documents the wrapper as <nav class="fdy-pagination" data-fdy-table-pagination>
|
|
88
|
+
and nothing in the kit ever wrote that class, here or in the three typed footers (#050 §2).
|
|
89
|
+
It carries no rule (NEXT-UP #9), so nothing rendered differently, but a consumer selector or
|
|
90
|
+
an e2e assertion on the block passed against hand-written markup and failed against the kit's
|
|
91
|
+
own output, and the raw and typed paths are supposed to be interchangeable. Set once at init,
|
|
92
|
+
not per render: the wrapper outlives every pager we build into it. */
|
|
93
|
+
if (pagerEl) pagerEl.classList.add('fdy-pagination');
|
|
87
94
|
var pageSizeEl = root.querySelector('[data-fdy-table-page-size]');
|
|
88
95
|
var selectAll = root.querySelector('[data-fdy-select-all]');
|
|
89
96
|
var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -323,7 +323,7 @@
|
|
|
323
323
|
--color-surface-raised: var(--color-surface);
|
|
324
324
|
--color-text: var(--slate-100);
|
|
325
325
|
--color-text-muted: var(--slate-400);
|
|
326
|
-
--color-text-subtle: var(--slate-
|
|
326
|
+
--color-text-subtle: var(--slate-450);
|
|
327
327
|
--color-on-brand: var(--slate-0);
|
|
328
328
|
--color-border: var(--slate-800);
|
|
329
329
|
--color-border-strong: var(--slate-700);
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
--color-surface-raised: var(--color-surface);
|
|
388
388
|
--color-text: var(--slate-100);
|
|
389
389
|
--color-text-muted: var(--slate-400);
|
|
390
|
-
--color-text-subtle: var(--slate-
|
|
390
|
+
--color-text-subtle: var(--slate-450);
|
|
391
391
|
--color-on-brand: var(--slate-0);
|
|
392
392
|
--color-border: var(--slate-800);
|
|
393
393
|
--color-border-strong: var(--slate-700);
|
|
@@ -1242,7 +1242,16 @@ a { color: var(--color-primary); }
|
|
|
1242
1242
|
|
|
1243
1243
|
/* The caption sits on a solid surface, not on the scrim. A scrim is semi-transparent over unknown
|
|
1244
1244
|
* content, so text on it has no computable contrast ratio and no gate could ever cover it. On
|
|
1245
|
-
* --color-surface it is the same pair the contrast suite already checks.
|
|
1245
|
+
* --color-surface it is the same pair the contrast suite already checks.
|
|
1246
|
+
*
|
|
1247
|
+
* THIS IS ALSO WHY THIS PANEL DOES NOT JOIN THE GLASS AXIS, and the omission is a decision rather
|
|
1248
|
+
* than an oversight. Every other raised panel reads --color-surface-raised and frosts under
|
|
1249
|
+
* [data-style="glass"]; this one stays opaque. The glass gate asserts its ink against the panel
|
|
1250
|
+
* composited over the PAGE GROUND (--color-surface-2), which is what sits behind a card or a modal.
|
|
1251
|
+
* Behind this one is the scrim, over arbitrary content — the very case that has no computable
|
|
1252
|
+
* ratio. A frosted busy panel would be the one surface in the kit whose caption nothing could
|
|
1253
|
+
* verify, while it is up precisely because the app is doing something the reader must not
|
|
1254
|
+
* interrupt. Left opaque on purpose; do not "fix" it for consistency. */
|
|
1246
1255
|
.fdy-busy__panel{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);padding:var(--space-6) var(--space-8);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lift);max-width:min(20rem,calc(100vw - var(--space-8)));}
|
|
1247
1256
|
.fdy-busy__mark{display:flex;align-items:center;justify-content:center;min-height:2.5rem;}
|
|
1248
1257
|
.fdy-busy__caption{margin:0;font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);text-align:center;}
|
|
@@ -1461,9 +1470,16 @@ a { color: var(--color-primary); }
|
|
|
1461
1470
|
/* Search bar, single leading-icon field; Enter/typing filters the results. */
|
|
1462
1471
|
.fdy-cfl__search{flex:none;padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
1463
1472
|
|
|
1464
|
-
/* Results, dense rows, sticky header, its own scroll region.
|
|
1465
|
-
|
|
1473
|
+
/* Results, dense rows, sticky header, its own scroll region.
|
|
1474
|
+
*
|
|
1475
|
+
* `container-type: inline-size` makes this box the query container for the stacked layout below.
|
|
1476
|
+
* Safe here and nowhere near free in general: a size container stops contributing intrinsic inline
|
|
1477
|
+
* size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
|
|
1478
|
+
* takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
|
|
1479
|
+
* its own content, so it never sized to the table to begin with. */
|
|
1480
|
+
.fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
|
|
1466
1481
|
.fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
|
|
1482
|
+
|
|
1467
1483
|
.fdy-cfl__row{cursor:pointer;}
|
|
1468
1484
|
.fdy-cfl__row:hover{background:var(--color-surface-2);}
|
|
1469
1485
|
.fdy-cfl__row:focus-visible{outline:none;background:var(--color-primary-soft);box-shadow:inset 2px 0 0 var(--color-primary);}
|
|
@@ -1481,6 +1497,54 @@ a { color: var(--color-primary); }
|
|
|
1481
1497
|
.fdy-cfl__count{font-size:var(--text-sm);color:var(--color-text-muted);}
|
|
1482
1498
|
.fdy-cfl__actions{display:flex;gap:var(--space-2);}
|
|
1483
1499
|
|
|
1500
|
+
/* Narrow: the same rows as list items, not table cells (#053).
|
|
1501
|
+
*
|
|
1502
|
+
* A 420px add-on panel gives this dialog ~372px. A three- or four-column master-data table in that
|
|
1503
|
+
* space either squeezes every column below legibility or scrolls on both axes, in a dialog whose
|
|
1504
|
+
* whole job is "search, look, click one". So the first cell becomes the title and the rest become
|
|
1505
|
+
* one detail line under it — the shape a picker takes when it is narrow.
|
|
1506
|
+
*
|
|
1507
|
+
* A CONTAINER query, not a media query: the dialog is not the viewport. The same component is wide
|
|
1508
|
+
* on a desktop page and narrow inside a docked panel, and only the container knows which.
|
|
1509
|
+
*
|
|
1510
|
+
* No ARIA roles come with this, and that is a MEASURED decision rather than an omission. The
|
|
1511
|
+
* received wisdom from the responsive-table era is that `display:block` on a table box drops its
|
|
1512
|
+
* implicit role; explicit `role="table"/"row"/"cell"` were written into all four stacks here on
|
|
1513
|
+
* that belief and then taken out again, because Chrome's own accessibility tree says otherwise:
|
|
1514
|
+
* with the rows at `display:flex`, stripping every `role` attribute leaves
|
|
1515
|
+
* `table → row → cell` intact (Chromium 133; only `rowgroup`, which associates nothing, is lost).
|
|
1516
|
+
* Redundant ARIA is what the kit's own rule calls worse than none, so the guard asserts the
|
|
1517
|
+
* OUTCOME — that a stacked row is still a row in the a11y tree — and stays silent about how.
|
|
1518
|
+
* `browser/cfl-narrow.mjs` is where a future engine that does dissolve them shows up as red.
|
|
1519
|
+
*
|
|
1520
|
+
* Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
|
|
1521
|
+
* row that costs a third of the width buys nothing when each row is labelled by its own title. */
|
|
1522
|
+
@container fdy-cfl (max-width: 30rem){
|
|
1523
|
+
.fdy-cfl__results .fdy-table,
|
|
1524
|
+
.fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
|
|
1525
|
+
.fdy-cfl__results .fdy-table thead{display:none;}
|
|
1526
|
+
.fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
1527
|
+
.fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
|
|
1528
|
+
.fdy-cfl__row > td{display:block;padding:0;border:0;}
|
|
1529
|
+
/* The title takes the whole first line, which is what pushes every remaining cell onto the second
|
|
1530
|
+
one — no selector has to know how many columns there are, or which of them is last. */
|
|
1531
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
|
|
1532
|
+
.fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
1533
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
|
|
1534
|
+
/* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
|
|
1535
|
+
.fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
|
|
1536
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
|
|
1537
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
1538
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
|
|
1539
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
|
|
1540
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
|
|
1541
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
|
|
1542
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
|
|
1543
|
+
.fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
|
|
1544
|
+
/* The checkbox keeps its own line position beside the title rather than above it. */
|
|
1545
|
+
.fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1484
1548
|
/* Freeday. Charts (zero-dependency CSS/SVG, rendered by freeday-chart.js).
|
|
1485
1549
|
* Colour comes from --fdy-chart (a token, set inline by the renderer from data-fdy-color). */
|
|
1486
1550
|
|
|
@@ -1670,9 +1734,22 @@ a { color: var(--color-primary); }
|
|
|
1670
1734
|
grid track, `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
1671
1735
|
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
1672
1736
|
@supports (container-type:inline-size){
|
|
1673
|
-
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
1674
|
-
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
1675
|
-
}
|
|
1737
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat{container-type:inline-size;}
|
|
1738
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
1739
|
+
}
|
|
1740
|
+
/* The price of that container, and the reason --inline opts out of it (#050). `container-type`
|
|
1741
|
+
applies inline-size CONTAINMENT: the tile's min-content and max-content contributions are both
|
|
1742
|
+
ZERO. So the 11rem floor above is not a taste choice, it is the only thing giving these tracks a
|
|
1743
|
+
size, and re-tracking the grid on content (`auto`, `max-content`, `minmax(0,…)`) resolves every
|
|
1744
|
+
track to 0. `justify-items` is the sharper edge: anything but the default `stretch` sizes the
|
|
1745
|
+
tile to that zero contribution even when the track is correctly wide, and three labels paint on
|
|
1746
|
+
top of each other. Measured from IDU_EMATE_ACCT_WEB's page header, where a hugging strip came out
|
|
1747
|
+
40px wide, all of it gap.
|
|
1748
|
+
--inline is the shape that header wanted: a strip that hugs its numbers and sits beside a page
|
|
1749
|
+
title, instead of a full-width band whose 11rem floor collapses to one column at 390px. It cannot
|
|
1750
|
+
have both, so it drops the container and keeps the flat --text-3xl the base rule already sets;
|
|
1751
|
+
a track sized to its own content is never the narrow track the clamp exists for. */
|
|
1752
|
+
.fdy-stats--inline{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:minmax(0,max-content);}
|
|
1676
1753
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
1677
1754
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
1678
1755
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -1878,8 +1955,9 @@ a { color: var(--color-primary); }
|
|
|
1878
1955
|
/* Fields are direct children. Drop the standalone field's 22rem cap; default to the
|
|
1879
1956
|
* medium column width with no unbounded growth (that's what caused the raggedness). */
|
|
1880
1957
|
.fdy-filterbar>.fdy-field{max-width:none;flex:0 0 auto;width:12rem;}
|
|
1881
|
-
/* Controls fill their field's column
|
|
1882
|
-
|
|
1958
|
+
/* Controls fill their field's column: released in input.css for every field, not just this one.
|
|
1959
|
+
The rule used to live here, naming the input group and the combo only, which is why an
|
|
1960
|
+
.fdy-autocomplete or an .fdy-cascade in a --w-2xl field still stopped 48px short of it (#051). */
|
|
1883
1961
|
/* Column-width rhythm, pick one per field so bars across the app line up. */
|
|
1884
1962
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
1885
1963
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
@@ -1956,6 +2034,15 @@ a { color: var(--color-primary); }
|
|
|
1956
2034
|
|
|
1957
2035
|
/* Freeday. Field & Input */
|
|
1958
2036
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
2037
|
+
/* Inside a labelled field, a control fills the field (#051). The 22rem cap those controls carry is
|
|
2038
|
+
for one standing on its own, with no field to take a width from; #017 already released the date
|
|
2039
|
+
and time pickers here for exactly this reason, and its comment claims a picker then fills its
|
|
2040
|
+
field "like every other control". That was not true of these four: a `--full` row in a
|
|
2041
|
+
.fdy-form-grid held a 352px combo under a 600px label, and a 26rem toolbar search field hid 64px
|
|
2042
|
+
of dead space INSIDE itself, so an author who wrote `gap:var(--space-3)` measured 76px on screen.
|
|
2043
|
+
The field's own max-width above is what governs, and it still caps a field nobody has widened, so
|
|
2044
|
+
this changes nothing wherever the cap was already doing its job. */
|
|
2045
|
+
.fdy-field>.fdy-input-group,.fdy-field>.fdy-combo,.fdy-field>.fdy-autocomplete,.fdy-field>.fdy-cascade{max-width:none;}
|
|
1959
2046
|
/* A grouped field is legitimately a <fieldset> (it needs a <legend>), e.g. a radio/checkbox group.
|
|
1960
2047
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1961
2048
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
@@ -2396,9 +2483,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
2396
2483
|
.fdy-table td>.fdy-input,.fdy-table td>.fdy-textarea,.fdy-table td>.fdy-combo,.fdy-table td>.fdy-input-group,.fdy-table td>.fdy-datepicker,.fdy-table td>.fdy-timepicker{min-width:7rem;}
|
|
2397
2484
|
|
|
2398
2485
|
/* Freeday. Tabs (WAI-ARIA APG) */
|
|
2399
|
-
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
2400
|
-
|
|
2401
|
-
|
|
2486
|
+
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
2487
|
+
*
|
|
2488
|
+
* The strip's own line is an INSET SHADOW, not a border, and the tab carries no negative margin
|
|
2489
|
+
* (#052). The pair those two replace — `border-bottom` here plus `margin-bottom:-1px` on the tab,
|
|
2490
|
+
* to lift the active tab's 2px underline onto the strip's 1px line so the two read as one — was
|
|
2491
|
+
* broken by the `overflow-x:auto` beside it, in two ways at once:
|
|
2492
|
+
* 1. CSS Overflow §3 gives no way to scroll one axis alone: `overflow-y` computes from `visible`
|
|
2493
|
+
* to `auto` because the other axis is not visible, so the 1px the tab hangs below the content
|
|
2494
|
+
* box became a real scrollport. Measured 47 client / 48 scroll, `scrollTop` settling at 1 —
|
|
2495
|
+
* invisible in a screenshot, and a trackpad gesture over the strip is eaten by it.
|
|
2496
|
+
* 2. A scroll container clips its descendants to the padding box, so the pixel the negative
|
|
2497
|
+
* margin pushed out was clipped away and the strip's own line painted in its place: the
|
|
2498
|
+
* active underline rendered ONE pixel of primary, never the two it asks for.
|
|
2499
|
+
* An inset shadow paints on the box, not on the scrolled content, so the line stays put while the
|
|
2500
|
+
* tabs scroll sideways, and the tab's own border-bottom paints over it — same look, no overflow.
|
|
2501
|
+
* To hide the line, override `box-shadow`, not `border-bottom` (guarded in browser/tabs-strip.mjs). */
|
|
2502
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);box-shadow:inset 0 calc(var(--bw) * -1) 0 var(--color-border);overflow-x:auto;}
|
|
2503
|
+
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
2402
2504
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
2403
2505
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
2404
2506
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
package/dist/freeday.css
CHANGED
|
@@ -364,7 +364,16 @@ a { color: var(--color-primary); }
|
|
|
364
364
|
|
|
365
365
|
/* The caption sits on a solid surface, not on the scrim. A scrim is semi-transparent over unknown
|
|
366
366
|
* content, so text on it has no computable contrast ratio and no gate could ever cover it. On
|
|
367
|
-
* --color-surface it is the same pair the contrast suite already checks.
|
|
367
|
+
* --color-surface it is the same pair the contrast suite already checks.
|
|
368
|
+
*
|
|
369
|
+
* THIS IS ALSO WHY THIS PANEL DOES NOT JOIN THE GLASS AXIS, and the omission is a decision rather
|
|
370
|
+
* than an oversight. Every other raised panel reads --color-surface-raised and frosts under
|
|
371
|
+
* [data-style="glass"]; this one stays opaque. The glass gate asserts its ink against the panel
|
|
372
|
+
* composited over the PAGE GROUND (--color-surface-2), which is what sits behind a card or a modal.
|
|
373
|
+
* Behind this one is the scrim, over arbitrary content — the very case that has no computable
|
|
374
|
+
* ratio. A frosted busy panel would be the one surface in the kit whose caption nothing could
|
|
375
|
+
* verify, while it is up precisely because the app is doing something the reader must not
|
|
376
|
+
* interrupt. Left opaque on purpose; do not "fix" it for consistency. */
|
|
368
377
|
.fdy-busy__panel{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);padding:var(--space-6) var(--space-8);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lift);max-width:min(20rem,calc(100vw - var(--space-8)));}
|
|
369
378
|
.fdy-busy__mark{display:flex;align-items:center;justify-content:center;min-height:2.5rem;}
|
|
370
379
|
.fdy-busy__caption{margin:0;font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);text-align:center;}
|
|
@@ -583,9 +592,16 @@ a { color: var(--color-primary); }
|
|
|
583
592
|
/* Search bar, single leading-icon field; Enter/typing filters the results. */
|
|
584
593
|
.fdy-cfl__search{flex:none;padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
585
594
|
|
|
586
|
-
/* Results, dense rows, sticky header, its own scroll region.
|
|
587
|
-
|
|
595
|
+
/* Results, dense rows, sticky header, its own scroll region.
|
|
596
|
+
*
|
|
597
|
+
* `container-type: inline-size` makes this box the query container for the stacked layout below.
|
|
598
|
+
* Safe here and nowhere near free in general: a size container stops contributing intrinsic inline
|
|
599
|
+
* size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
|
|
600
|
+
* takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
|
|
601
|
+
* its own content, so it never sized to the table to begin with. */
|
|
602
|
+
.fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
|
|
588
603
|
.fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
|
|
604
|
+
|
|
589
605
|
.fdy-cfl__row{cursor:pointer;}
|
|
590
606
|
.fdy-cfl__row:hover{background:var(--color-surface-2);}
|
|
591
607
|
.fdy-cfl__row:focus-visible{outline:none;background:var(--color-primary-soft);box-shadow:inset 2px 0 0 var(--color-primary);}
|
|
@@ -603,6 +619,54 @@ a { color: var(--color-primary); }
|
|
|
603
619
|
.fdy-cfl__count{font-size:var(--text-sm);color:var(--color-text-muted);}
|
|
604
620
|
.fdy-cfl__actions{display:flex;gap:var(--space-2);}
|
|
605
621
|
|
|
622
|
+
/* Narrow: the same rows as list items, not table cells (#053).
|
|
623
|
+
*
|
|
624
|
+
* A 420px add-on panel gives this dialog ~372px. A three- or four-column master-data table in that
|
|
625
|
+
* space either squeezes every column below legibility or scrolls on both axes, in a dialog whose
|
|
626
|
+
* whole job is "search, look, click one". So the first cell becomes the title and the rest become
|
|
627
|
+
* one detail line under it — the shape a picker takes when it is narrow.
|
|
628
|
+
*
|
|
629
|
+
* A CONTAINER query, not a media query: the dialog is not the viewport. The same component is wide
|
|
630
|
+
* on a desktop page and narrow inside a docked panel, and only the container knows which.
|
|
631
|
+
*
|
|
632
|
+
* No ARIA roles come with this, and that is a MEASURED decision rather than an omission. The
|
|
633
|
+
* received wisdom from the responsive-table era is that `display:block` on a table box drops its
|
|
634
|
+
* implicit role; explicit `role="table"/"row"/"cell"` were written into all four stacks here on
|
|
635
|
+
* that belief and then taken out again, because Chrome's own accessibility tree says otherwise:
|
|
636
|
+
* with the rows at `display:flex`, stripping every `role` attribute leaves
|
|
637
|
+
* `table → row → cell` intact (Chromium 133; only `rowgroup`, which associates nothing, is lost).
|
|
638
|
+
* Redundant ARIA is what the kit's own rule calls worse than none, so the guard asserts the
|
|
639
|
+
* OUTCOME — that a stacked row is still a row in the a11y tree — and stays silent about how.
|
|
640
|
+
* `browser/cfl-narrow.mjs` is where a future engine that does dissolve them shows up as red.
|
|
641
|
+
*
|
|
642
|
+
* Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
|
|
643
|
+
* row that costs a third of the width buys nothing when each row is labelled by its own title. */
|
|
644
|
+
@container fdy-cfl (max-width: 30rem){
|
|
645
|
+
.fdy-cfl__results .fdy-table,
|
|
646
|
+
.fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
|
|
647
|
+
.fdy-cfl__results .fdy-table thead{display:none;}
|
|
648
|
+
.fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
649
|
+
.fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
|
|
650
|
+
.fdy-cfl__row > td{display:block;padding:0;border:0;}
|
|
651
|
+
/* The title takes the whole first line, which is what pushes every remaining cell onto the second
|
|
652
|
+
one — no selector has to know how many columns there are, or which of them is last. */
|
|
653
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
|
|
654
|
+
.fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
655
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
|
|
656
|
+
/* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
|
|
657
|
+
.fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
|
|
658
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
|
|
659
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
660
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
|
|
661
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
|
|
662
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
|
|
663
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
|
|
664
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
|
|
665
|
+
.fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
|
|
666
|
+
/* The checkbox keeps its own line position beside the title rather than above it. */
|
|
667
|
+
.fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
|
|
668
|
+
}
|
|
669
|
+
|
|
606
670
|
/* Freeday. Charts (zero-dependency CSS/SVG, rendered by freeday-chart.js).
|
|
607
671
|
* Colour comes from --fdy-chart (a token, set inline by the renderer from data-fdy-color). */
|
|
608
672
|
|
|
@@ -792,9 +856,22 @@ a { color: var(--color-primary); }
|
|
|
792
856
|
grid track, `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
793
857
|
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
794
858
|
@supports (container-type:inline-size){
|
|
795
|
-
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
796
|
-
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
859
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat{container-type:inline-size;}
|
|
860
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
797
861
|
}
|
|
862
|
+
/* The price of that container, and the reason --inline opts out of it (#050). `container-type`
|
|
863
|
+
applies inline-size CONTAINMENT: the tile's min-content and max-content contributions are both
|
|
864
|
+
ZERO. So the 11rem floor above is not a taste choice, it is the only thing giving these tracks a
|
|
865
|
+
size, and re-tracking the grid on content (`auto`, `max-content`, `minmax(0,…)`) resolves every
|
|
866
|
+
track to 0. `justify-items` is the sharper edge: anything but the default `stretch` sizes the
|
|
867
|
+
tile to that zero contribution even when the track is correctly wide, and three labels paint on
|
|
868
|
+
top of each other. Measured from IDU_EMATE_ACCT_WEB's page header, where a hugging strip came out
|
|
869
|
+
40px wide, all of it gap.
|
|
870
|
+
--inline is the shape that header wanted: a strip that hugs its numbers and sits beside a page
|
|
871
|
+
title, instead of a full-width band whose 11rem floor collapses to one column at 390px. It cannot
|
|
872
|
+
have both, so it drops the container and keeps the flat --text-3xl the base rule already sets;
|
|
873
|
+
a track sized to its own content is never the narrow track the clamp exists for. */
|
|
874
|
+
.fdy-stats--inline{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:minmax(0,max-content);}
|
|
798
875
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
799
876
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
800
877
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -1000,8 +1077,9 @@ a { color: var(--color-primary); }
|
|
|
1000
1077
|
/* Fields are direct children. Drop the standalone field's 22rem cap; default to the
|
|
1001
1078
|
* medium column width with no unbounded growth (that's what caused the raggedness). */
|
|
1002
1079
|
.fdy-filterbar>.fdy-field{max-width:none;flex:0 0 auto;width:12rem;}
|
|
1003
|
-
/* Controls fill their field's column
|
|
1004
|
-
|
|
1080
|
+
/* Controls fill their field's column: released in input.css for every field, not just this one.
|
|
1081
|
+
The rule used to live here, naming the input group and the combo only, which is why an
|
|
1082
|
+
.fdy-autocomplete or an .fdy-cascade in a --w-2xl field still stopped 48px short of it (#051). */
|
|
1005
1083
|
/* Column-width rhythm, pick one per field so bars across the app line up. */
|
|
1006
1084
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
1007
1085
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
|
@@ -1078,6 +1156,15 @@ a { color: var(--color-primary); }
|
|
|
1078
1156
|
|
|
1079
1157
|
/* Freeday. Field & Input */
|
|
1080
1158
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
1159
|
+
/* Inside a labelled field, a control fills the field (#051). The 22rem cap those controls carry is
|
|
1160
|
+
for one standing on its own, with no field to take a width from; #017 already released the date
|
|
1161
|
+
and time pickers here for exactly this reason, and its comment claims a picker then fills its
|
|
1162
|
+
field "like every other control". That was not true of these four: a `--full` row in a
|
|
1163
|
+
.fdy-form-grid held a 352px combo under a 600px label, and a 26rem toolbar search field hid 64px
|
|
1164
|
+
of dead space INSIDE itself, so an author who wrote `gap:var(--space-3)` measured 76px on screen.
|
|
1165
|
+
The field's own max-width above is what governs, and it still caps a field nobody has widened, so
|
|
1166
|
+
this changes nothing wherever the cap was already doing its job. */
|
|
1167
|
+
.fdy-field>.fdy-input-group,.fdy-field>.fdy-combo,.fdy-field>.fdy-autocomplete,.fdy-field>.fdy-cascade{max-width:none;}
|
|
1081
1168
|
/* A grouped field is legitimately a <fieldset> (it needs a <legend>), e.g. a radio/checkbox group.
|
|
1082
1169
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
1083
1170
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
|
@@ -1518,9 +1605,24 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1518
1605
|
.fdy-table td>.fdy-input,.fdy-table td>.fdy-textarea,.fdy-table td>.fdy-combo,.fdy-table td>.fdy-input-group,.fdy-table td>.fdy-datepicker,.fdy-table td>.fdy-timepicker{min-width:7rem;}
|
|
1519
1606
|
|
|
1520
1607
|
/* Freeday. Tabs (WAI-ARIA APG) */
|
|
1521
|
-
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
1522
|
-
|
|
1523
|
-
|
|
1608
|
+
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
1609
|
+
*
|
|
1610
|
+
* The strip's own line is an INSET SHADOW, not a border, and the tab carries no negative margin
|
|
1611
|
+
* (#052). The pair those two replace — `border-bottom` here plus `margin-bottom:-1px` on the tab,
|
|
1612
|
+
* to lift the active tab's 2px underline onto the strip's 1px line so the two read as one — was
|
|
1613
|
+
* broken by the `overflow-x:auto` beside it, in two ways at once:
|
|
1614
|
+
* 1. CSS Overflow §3 gives no way to scroll one axis alone: `overflow-y` computes from `visible`
|
|
1615
|
+
* to `auto` because the other axis is not visible, so the 1px the tab hangs below the content
|
|
1616
|
+
* box became a real scrollport. Measured 47 client / 48 scroll, `scrollTop` settling at 1 —
|
|
1617
|
+
* invisible in a screenshot, and a trackpad gesture over the strip is eaten by it.
|
|
1618
|
+
* 2. A scroll container clips its descendants to the padding box, so the pixel the negative
|
|
1619
|
+
* margin pushed out was clipped away and the strip's own line painted in its place: the
|
|
1620
|
+
* active underline rendered ONE pixel of primary, never the two it asks for.
|
|
1621
|
+
* An inset shadow paints on the box, not on the scrolled content, so the line stays put while the
|
|
1622
|
+
* tabs scroll sideways, and the tab's own border-bottom paints over it — same look, no overflow.
|
|
1623
|
+
* To hide the line, override `box-shadow`, not `border-bottom` (guarded in browser/tabs-strip.mjs). */
|
|
1624
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);box-shadow:inset 0 calc(var(--bw) * -1) 0 var(--color-border);overflow-x:auto;}
|
|
1625
|
+
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
1524
1626
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
1525
1627
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
1526
1628
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
package/dist/freeday.js
CHANGED
|
@@ -3905,6 +3905,13 @@
|
|
|
3905
3905
|
var countEl = root.querySelector('[data-fdy-table-count]');
|
|
3906
3906
|
var infoEl = root.querySelector('[data-fdy-table-info]');
|
|
3907
3907
|
var pagerEl = root.querySelector('[data-fdy-table-pagination]');
|
|
3908
|
+
/* COMPONENTS.md documents the wrapper as <nav class="fdy-pagination" data-fdy-table-pagination>
|
|
3909
|
+
and nothing in the kit ever wrote that class, here or in the three typed footers (#050 §2).
|
|
3910
|
+
It carries no rule (NEXT-UP #9), so nothing rendered differently, but a consumer selector or
|
|
3911
|
+
an e2e assertion on the block passed against hand-written markup and failed against the kit's
|
|
3912
|
+
own output, and the raw and typed paths are supposed to be interchangeable. Set once at init,
|
|
3913
|
+
not per render: the wrapper outlives every pager we build into it. */
|
|
3914
|
+
if (pagerEl) pagerEl.classList.add('fdy-pagination');
|
|
3908
3915
|
var pageSizeEl = root.querySelector('[data-fdy-table-page-size]');
|
|
3909
3916
|
var selectAll = root.querySelector('[data-fdy-select-all]');
|
|
3910
3917
|
var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
|
package/dist/freeday.tokens.css
CHANGED
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
--color-surface-raised: var(--color-surface);
|
|
323
323
|
--color-text: var(--slate-100);
|
|
324
324
|
--color-text-muted: var(--slate-400);
|
|
325
|
-
--color-text-subtle: var(--slate-
|
|
325
|
+
--color-text-subtle: var(--slate-450);
|
|
326
326
|
--color-on-brand: var(--slate-0);
|
|
327
327
|
--color-border: var(--slate-800);
|
|
328
328
|
--color-border-strong: var(--slate-700);
|
|
@@ -386,7 +386,7 @@
|
|
|
386
386
|
--color-surface-raised: var(--color-surface);
|
|
387
387
|
--color-text: var(--slate-100);
|
|
388
388
|
--color-text-muted: var(--slate-400);
|
|
389
|
-
--color-text-subtle: var(--slate-
|
|
389
|
+
--color-text-subtle: var(--slate-450);
|
|
390
390
|
--color-on-brand: var(--slate-0);
|
|
391
391
|
--color-border: var(--slate-800);
|
|
392
392
|
--color-border-strong: var(--slate-700);
|
package/docs/getting-started.md
CHANGED
|
@@ -191,7 +191,7 @@ live docs also have a copy button per component.
|
|
|
191
191
|
```bash
|
|
192
192
|
npm i @cahyo-dimas/freeday
|
|
193
193
|
```
|
|
194
|
-
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.
|
|
194
|
+
Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.2.0"` (public npm package). `dist/` is
|
|
195
195
|
committed and published → no build step; `npm ci` runs without auth.
|
|
196
196
|
|
|
197
197
|
### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cahyo-dimas/freeday",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"scripts": {
|
|
93
93
|
"build": "node tokens/build.mjs",
|
|
94
94
|
"test": "node --test",
|
|
95
|
-
"test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/picker-states.mjs browser/harness-contract.mjs",
|
|
95
|
+
"test:browser": "node --test --test-concurrency=3 browser/vanilla.mjs browser/adapter.mjs browser/layout.mjs browser/theme.mjs browser/state.mjs browser/root-init.mjs browser/upload-states.mjs browser/number.mjs browser/card-stretch.mjs browser/text-override.mjs browser/control-heights.mjs browser/field-width.mjs browser/cfl-multi.mjs browser/crowding.mjs browser/over-dialog.mjs browser/chart-scale.mjs browser/overlay-stack.mjs browser/chart-a11y.mjs browser/app-shell.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/cfl-narrow.mjs browser/harness-contract.mjs",
|
|
96
96
|
"prepack": "node tokens/build.mjs",
|
|
97
97
|
"version": "node tokens/build.mjs && git add dist",
|
|
98
98
|
"typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
|
package/src/components/busy.css
CHANGED
|
@@ -21,7 +21,16 @@
|
|
|
21
21
|
|
|
22
22
|
/* The caption sits on a solid surface, not on the scrim. A scrim is semi-transparent over unknown
|
|
23
23
|
* content, so text on it has no computable contrast ratio and no gate could ever cover it. On
|
|
24
|
-
* --color-surface it is the same pair the contrast suite already checks.
|
|
24
|
+
* --color-surface it is the same pair the contrast suite already checks.
|
|
25
|
+
*
|
|
26
|
+
* THIS IS ALSO WHY THIS PANEL DOES NOT JOIN THE GLASS AXIS, and the omission is a decision rather
|
|
27
|
+
* than an oversight. Every other raised panel reads --color-surface-raised and frosts under
|
|
28
|
+
* [data-style="glass"]; this one stays opaque. The glass gate asserts its ink against the panel
|
|
29
|
+
* composited over the PAGE GROUND (--color-surface-2), which is what sits behind a card or a modal.
|
|
30
|
+
* Behind this one is the scrim, over arbitrary content — the very case that has no computable
|
|
31
|
+
* ratio. A frosted busy panel would be the one surface in the kit whose caption nothing could
|
|
32
|
+
* verify, while it is up precisely because the app is doing something the reader must not
|
|
33
|
+
* interrupt. Left opaque on purpose; do not "fix" it for consistency. */
|
|
25
34
|
.fdy-busy__panel{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);padding:var(--space-6) var(--space-8);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lift);max-width:min(20rem,calc(100vw - var(--space-8)));}
|
|
26
35
|
.fdy-busy__mark{display:flex;align-items:center;justify-content:center;min-height:2.5rem;}
|
|
27
36
|
.fdy-busy__caption{margin:0;font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text);text-align:center;}
|
package/src/components/cfl.css
CHANGED
|
@@ -7,9 +7,16 @@
|
|
|
7
7
|
/* Search bar, single leading-icon field; Enter/typing filters the results. */
|
|
8
8
|
.fdy-cfl__search{flex:none;padding:var(--space-4) var(--space-5);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
9
9
|
|
|
10
|
-
/* Results, dense rows, sticky header, its own scroll region.
|
|
11
|
-
|
|
10
|
+
/* Results, dense rows, sticky header, its own scroll region.
|
|
11
|
+
*
|
|
12
|
+
* `container-type: inline-size` makes this box the query container for the stacked layout below.
|
|
13
|
+
* Safe here and nowhere near free in general: a size container stops contributing intrinsic inline
|
|
14
|
+
* size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
|
|
15
|
+
* takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
|
|
16
|
+
* its own content, so it never sized to the table to begin with. */
|
|
17
|
+
.fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
|
|
12
18
|
.fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
|
|
19
|
+
|
|
13
20
|
.fdy-cfl__row{cursor:pointer;}
|
|
14
21
|
.fdy-cfl__row:hover{background:var(--color-surface-2);}
|
|
15
22
|
.fdy-cfl__row:focus-visible{outline:none;background:var(--color-primary-soft);box-shadow:inset 2px 0 0 var(--color-primary);}
|
|
@@ -26,3 +33,51 @@
|
|
|
26
33
|
.fdy-modal--cfl .fdy-modal__footer{justify-content:space-between;}
|
|
27
34
|
.fdy-cfl__count{font-size:var(--text-sm);color:var(--color-text-muted);}
|
|
28
35
|
.fdy-cfl__actions{display:flex;gap:var(--space-2);}
|
|
36
|
+
|
|
37
|
+
/* Narrow: the same rows as list items, not table cells (#053).
|
|
38
|
+
*
|
|
39
|
+
* A 420px add-on panel gives this dialog ~372px. A three- or four-column master-data table in that
|
|
40
|
+
* space either squeezes every column below legibility or scrolls on both axes, in a dialog whose
|
|
41
|
+
* whole job is "search, look, click one". So the first cell becomes the title and the rest become
|
|
42
|
+
* one detail line under it — the shape a picker takes when it is narrow.
|
|
43
|
+
*
|
|
44
|
+
* A CONTAINER query, not a media query: the dialog is not the viewport. The same component is wide
|
|
45
|
+
* on a desktop page and narrow inside a docked panel, and only the container knows which.
|
|
46
|
+
*
|
|
47
|
+
* No ARIA roles come with this, and that is a MEASURED decision rather than an omission. The
|
|
48
|
+
* received wisdom from the responsive-table era is that `display:block` on a table box drops its
|
|
49
|
+
* implicit role; explicit `role="table"/"row"/"cell"` were written into all four stacks here on
|
|
50
|
+
* that belief and then taken out again, because Chrome's own accessibility tree says otherwise:
|
|
51
|
+
* with the rows at `display:flex`, stripping every `role` attribute leaves
|
|
52
|
+
* `table → row → cell` intact (Chromium 133; only `rowgroup`, which associates nothing, is lost).
|
|
53
|
+
* Redundant ARIA is what the kit's own rule calls worse than none, so the guard asserts the
|
|
54
|
+
* OUTCOME — that a stacked row is still a row in the a11y tree — and stays silent about how.
|
|
55
|
+
* `browser/cfl-narrow.mjs` is where a future engine that does dissolve them shows up as red.
|
|
56
|
+
*
|
|
57
|
+
* Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
|
|
58
|
+
* row that costs a third of the width buys nothing when each row is labelled by its own title. */
|
|
59
|
+
@container fdy-cfl (max-width: 30rem){
|
|
60
|
+
.fdy-cfl__results .fdy-table,
|
|
61
|
+
.fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
|
|
62
|
+
.fdy-cfl__results .fdy-table thead{display:none;}
|
|
63
|
+
.fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
64
|
+
.fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
|
|
65
|
+
.fdy-cfl__row > td{display:block;padding:0;border:0;}
|
|
66
|
+
/* The title takes the whole first line, which is what pushes every remaining cell onto the second
|
|
67
|
+
one — no selector has to know how many columns there are, or which of them is last. */
|
|
68
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
|
|
69
|
+
.fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
|
|
70
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
|
|
71
|
+
/* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
|
|
72
|
+
.fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
|
|
73
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
|
|
74
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
|
|
75
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
|
|
76
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
|
|
77
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
|
|
78
|
+
.fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
|
|
79
|
+
.fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
|
|
80
|
+
.fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
|
|
81
|
+
/* The checkbox keeps its own line position beside the title rather than above it. */
|
|
82
|
+
.fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
|
|
83
|
+
}
|
|
@@ -60,9 +60,22 @@
|
|
|
60
60
|
grid track, `container-type:inline-size` on a standalone .fdy-stat would stop it sizing to its
|
|
61
61
|
own content. The unconditional rule above stays as the fallback wherever @container is missing. */
|
|
62
62
|
@supports (container-type:inline-size){
|
|
63
|
-
.fdy-stats>.fdy-stat{container-type:inline-size;}
|
|
64
|
-
.fdy-stats>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
63
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat{container-type:inline-size;}
|
|
64
|
+
.fdy-stats:not(.fdy-stats--inline)>.fdy-stat .fdy-stat__value{font-size:clamp(var(--text-xl),11cqw,var(--text-3xl));}
|
|
65
65
|
}
|
|
66
|
+
/* The price of that container, and the reason --inline opts out of it (#050). `container-type`
|
|
67
|
+
applies inline-size CONTAINMENT: the tile's min-content and max-content contributions are both
|
|
68
|
+
ZERO. So the 11rem floor above is not a taste choice, it is the only thing giving these tracks a
|
|
69
|
+
size, and re-tracking the grid on content (`auto`, `max-content`, `minmax(0,…)`) resolves every
|
|
70
|
+
track to 0. `justify-items` is the sharper edge: anything but the default `stretch` sizes the
|
|
71
|
+
tile to that zero contribution even when the track is correctly wide, and three labels paint on
|
|
72
|
+
top of each other. Measured from IDU_EMATE_ACCT_WEB's page header, where a hugging strip came out
|
|
73
|
+
40px wide, all of it gap.
|
|
74
|
+
--inline is the shape that header wanted: a strip that hugs its numbers and sits beside a page
|
|
75
|
+
title, instead of a full-width band whose 11rem floor collapses to one column at 390px. It cannot
|
|
76
|
+
have both, so it drops the container and keeps the flat --text-3xl the base rule already sets;
|
|
77
|
+
a track sized to its own content is never the narrow track the clamp exists for. */
|
|
78
|
+
.fdy-stats--inline{grid-template-columns:none;grid-auto-flow:column;grid-auto-columns:minmax(0,max-content);}
|
|
66
79
|
.fdy-stats--boxed{background:var(--color-surface);border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);gap:0;}
|
|
67
80
|
.fdy-stats--boxed .fdy-stat{padding:var(--space-5);border-right:var(--bw) solid var(--color-border-muted);}
|
|
68
81
|
.fdy-stats--boxed .fdy-stat:last-child{border-right:0;}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
/* Fields are direct children. Drop the standalone field's 22rem cap; default to the
|
|
14
14
|
* medium column width with no unbounded growth (that's what caused the raggedness). */
|
|
15
15
|
.fdy-filterbar>.fdy-field{max-width:none;flex:0 0 auto;width:12rem;}
|
|
16
|
-
/* Controls fill their field's column
|
|
17
|
-
|
|
16
|
+
/* Controls fill their field's column: released in input.css for every field, not just this one.
|
|
17
|
+
The rule used to live here, naming the input group and the combo only, which is why an
|
|
18
|
+
.fdy-autocomplete or an .fdy-cascade in a --w-2xl field still stopped 48px short of it (#051). */
|
|
18
19
|
/* Column-width rhythm, pick one per field so bars across the app line up. */
|
|
19
20
|
.fdy-filterbar>.fdy-field--w-sm{width:9rem;}
|
|
20
21
|
.fdy-filterbar>.fdy-field--w-lg{width:15rem;}
|
package/src/components/input.css
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/* Freeday. Field & Input */
|
|
2
2
|
.fdy-field{display:flex;flex-direction:column;gap:var(--space-2);max-width:22rem;}
|
|
3
|
+
/* Inside a labelled field, a control fills the field (#051). The 22rem cap those controls carry is
|
|
4
|
+
for one standing on its own, with no field to take a width from; #017 already released the date
|
|
5
|
+
and time pickers here for exactly this reason, and its comment claims a picker then fills its
|
|
6
|
+
field "like every other control". That was not true of these four: a `--full` row in a
|
|
7
|
+
.fdy-form-grid held a 352px combo under a 600px label, and a 26rem toolbar search field hid 64px
|
|
8
|
+
of dead space INSIDE itself, so an author who wrote `gap:var(--space-3)` measured 76px on screen.
|
|
9
|
+
The field's own max-width above is what governs, and it still caps a field nobody has widened, so
|
|
10
|
+
this changes nothing wherever the cap was already doing its job. */
|
|
11
|
+
.fdy-field>.fdy-input-group,.fdy-field>.fdy-combo,.fdy-field>.fdy-autocomplete,.fdy-field>.fdy-cascade{max-width:none;}
|
|
3
12
|
/* A grouped field is legitimately a <fieldset> (it needs a <legend>), e.g. a radio/checkbox group.
|
|
4
13
|
Neutralise the UA groove border / padding / min-inline-size so it lays out identically to the
|
|
5
14
|
<div> form (min-inline-size:min-content would otherwise block the filterbar column widths). */
|
package/src/components/tabs.css
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
/* Freeday. Tabs (WAI-ARIA APG) */
|
|
2
|
-
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/* position:relative, containing block for out-of-flow content in the tabs (see base.css).
|
|
3
|
+
*
|
|
4
|
+
* The strip's own line is an INSET SHADOW, not a border, and the tab carries no negative margin
|
|
5
|
+
* (#052). The pair those two replace — `border-bottom` here plus `margin-bottom:-1px` on the tab,
|
|
6
|
+
* to lift the active tab's 2px underline onto the strip's 1px line so the two read as one — was
|
|
7
|
+
* broken by the `overflow-x:auto` beside it, in two ways at once:
|
|
8
|
+
* 1. CSS Overflow §3 gives no way to scroll one axis alone: `overflow-y` computes from `visible`
|
|
9
|
+
* to `auto` because the other axis is not visible, so the 1px the tab hangs below the content
|
|
10
|
+
* box became a real scrollport. Measured 47 client / 48 scroll, `scrollTop` settling at 1 —
|
|
11
|
+
* invisible in a screenshot, and a trackpad gesture over the strip is eaten by it.
|
|
12
|
+
* 2. A scroll container clips its descendants to the padding box, so the pixel the negative
|
|
13
|
+
* margin pushed out was clipped away and the strip's own line painted in its place: the
|
|
14
|
+
* active underline rendered ONE pixel of primary, never the two it asks for.
|
|
15
|
+
* An inset shadow paints on the box, not on the scrolled content, so the line stays put while the
|
|
16
|
+
* tabs scroll sideways, and the tab's own border-bottom paints over it — same look, no overflow.
|
|
17
|
+
* To hide the line, override `box-shadow`, not `border-bottom` (guarded in browser/tabs-strip.mjs). */
|
|
18
|
+
.fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);box-shadow:inset 0 calc(var(--bw) * -1) 0 var(--color-border);overflow-x:auto;}
|
|
19
|
+
.fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
|
|
5
20
|
.fdy-tabs__tab:hover{color:var(--color-text);}
|
|
6
21
|
.fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
|
|
7
22
|
.fdy-tabs__tab:disabled:hover,.fdy-tabs__tab[aria-disabled="true"]:hover{color:var(--color-text-muted);}
|
package/tokens/tokens.json
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
|
|
123
123
|
"text": { "$value": "{slate.900}", "$dark": "{slate.100}" },
|
|
124
124
|
"text-muted": { "$value": "{slate.600}", "$dark": "{slate.400}" },
|
|
125
|
-
"text-subtle": { "$value": "{slate.500}", "$dark": "{slate.
|
|
125
|
+
"text-subtle": { "$comment": "The faintest ink, and the ONLY text tier whose dark value is not the light one's mirror (#053). Light slate-500 on white is 4.69:1 and clears AA on its own; the same primitive on the dark surface is 4.02:1, and the tier is exactly what a placeholder uses — the one line telling an operator what an empty field wants. Dark is slate-450 for that reason: 4.88:1 on --color-surface, which is what every .fdy-input paints itself, so the placeholder clears AA where placeholders actually sit. It is still NOT a body-text tier on the darker surfaces (3.66:1 on surface-3); reaching AA there needs slate-400, which is --color-text-muted, and a subtle that equals muted is not a tier. test/contrast.test.mjs asserts both halves of that split.", "$value": "{slate.500}", "$dark": "{slate.450}" },
|
|
126
126
|
"on-brand": { "$value": "{slate.0}", "$dark": "{slate.0}" },
|
|
127
127
|
|
|
128
128
|
"border": { "$value": "{slate.200}", "$dark": "{slate.800}" },
|