@cahyo-dimas/freeday 3.3.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,89 @@ 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.5.0] - 2026-09-15
13
+
14
+ **Sebuah angka yang hanya bisa ditemukan dengan membaca stylesheet kit.** `#058` (dari
15
+ `IDU_AI_DOC_SAPB1_CLIENT` dan `IDU_AI_DOC_SAAS`, satu sapuan atas dua konsol) datang dari audit yang
16
+ hasilnya nyaris bersih: tipografi, spasi dan warna tak menyisakan satu pun nilai mentah. Yang tersisa
17
+ cuma media query — dan di situlah dua hal yang sama-sama berakar pada satu kenyataan CSS: `@media`
18
+ tak bisa membaca custom property.
19
+
20
+ Akibatnya angka breakpoint kit hanya hidup sebagai literal, app mengetik ulang literal itu, dan
21
+ literal yang diketik ulang hanyut. Dua konsol itu menuliskan satu lebar yang sama dengan tiga cara
22
+ (`640px`, `40rem`, `45rem`) untuk satu maksud yang sama.
23
+
24
+ ### Added
25
+ - **`@cahyo-dimas/freeday/media`** — berkas `@custom-media` (`dist/freeday.media.css`), jawaban
26
+ sisi-CSS untuk apa yang selama ini hanya dijawab `tokens/breakpoints.mjs` di sisi JS. App menulis
27
+ `@media (--fdy-below-sm)` atau `@media (--fdy-filterbar-stacked)`, bukan literal yang diketik
28
+ ulang, dan karenanya tak bisa hanyut.
29
+ Isinya `--fdy-below-*` / `--fdy-from-*` untuk ramp (`sm`·`md`·`lg`·`xl`), plus `--fdy-nav-drawer`,
30
+ `--fdy-nav-static` dan `--fdy-filterbar-stacked` untuk dua lebar yang bukan anak tangga ramp.
31
+ **Opt-in, dan tak mengubah apa pun bagi yang tidak memakainya:** `@custom-media` adalah transform
32
+ PostCSS (`postcss-custom-media`, sudah termasuk di `postcss-preset-env`), jadi app ber-Vite
33
+ mendapatkannya cuma-cuma dan app tanpa PostCSS tetap menulis literal seperti kemarin. Berkas ini
34
+ **tidak** ikut `freeday.css` maupun `freeday.bundle.css` — at-rule waktu-build tak punya urusan di
35
+ stylesheet yang dimuat tiap app — dan tak membawa satu rule pun, jadi meng-import-nya tak berbiaya
36
+ saat runtime.
37
+ Di-**generate** dari objek yang sama yang diekspor sisi JS, jadi kedua paruh skala itu tak mungkin
38
+ berbeda pendapat; yang menjaganya agar tetap sama dengan CSS yang dideskripsikannya adalah dua tes
39
+ baru di `test/docs.test.mjs`, diverifikasi dengan mutasi.
40
+ - **`breakpoints.filterbar` (640)** — lebar tempat `.fdy-filterbar` menumpuk fieldnya jadi selebar
41
+ penuh, kini bernama, bukan cuma tertulis di `filterbar.css:45`.
42
+
43
+ ### Changed
44
+ - **640 tetap 640, tapi berhenti jadi angka siluman.** Ia bukan anak tangga ramp (`sm` = 600) dan
45
+ sengaja tidak dipindahkan: 640 adalah lebar telepon-lanskap yang nyata, dan menggesernya akan
46
+ me-reflow setiap filter bar di setiap app. Yang salah bukan angkanya, melainkan bahwa satu-satunya
47
+ cara menemukannya adalah membaca stylesheet kit lalu mempercayai sumber di atas dokumentasi.
48
+ Menyelaraskan rule app ke `sm` — hal yang wajar dilakukan dari dokumentasi saja — merusak
49
+ 600–640px: fieldnya sudah ditumpuk kit sementara app masih membatasi lebar field `--w-grow`-nya,
50
+ sehingga field itu terlihat lebih sempit dari tetangganya di tumpukan.
51
+ Header `breakpoints.css`, komentar `filterbar.css`, `COMPONENTS.md` dan kedua README kini menyebut
52
+ `nav` **dan** `filterbar` sebagai dua lebar yang bukan ramp, lengkap dengan pita px yang rusak
53
+ kalau dibulatkan ke anak tangga terdekat.
54
+
55
+ ## [3.4.0] - 2026-09-07
56
+
57
+ **Tiga hal yang hanya terlihat setelah sebuah app bertemu telepon sungguhan.** `#057` (dari
58
+ `keamanan-cluster`) datang dari pemilik yang menguji di iPhone X, dan ketiganya punya bentuk yang
59
+ sama: tak satu pun dapat ditemukan di Chromium, dan tak satu pun akan pernah ditangkap uji yang
60
+ sudah ada — bukan karena ujinya kurang, melainkan karena mesin peramban di laptop tidak berperilaku
61
+ seperti Safari di HP, dan laptop tidak punya ibu jari.
62
+
63
+ Yang pertama mengunci navigasi sepenuhnya: tap di luar drawer tidak menutupnya, layar tetap gelap,
64
+ dan satu-satunya jalan keluar adalah memuat ulang halaman.
65
+
66
+ ### Added
67
+ - **`.fdy-bottomnav`** — bilah navigasi bawah untuk aplikasi di HP. Kit punya `--horizontal` untuk
68
+ top-nav dan drawer off-canvas untuk sidebar, dan tidak punya apa pun untuk susunan yang justru
69
+ dipakai hampir semua aplikasi telepon. Tampil hanya di `max-width:720px`; di desktop sidebar sudah
70
+ membawa tautan yang sama dan salinan kedua hanya duplikasi.
71
+ Sengaja **bukan** bagian markup `.fdy-app`: ia `position:fixed` sehingga bekerja dari mana pun di
72
+ dalam shell, dan aplikasi yang tidak memakainya tidak membayar apa pun.
73
+ Target minimal 44px — di bawah itu satu tap menjadi satu percobaan ulang, dan bilah ini ditekan
74
+ lebih sering daripada kontrol mana pun. Label tidak opsional: bilah ikon-saja menuntut setiap
75
+ pengguna sudah hafal arti tiap glyph. `env(safe-area-inset-bottom)` masuk ke padding, jadi label
76
+ tidak duduk di bawah home indicator pada HP bernotch.
77
+ - **`.fdy-app--has-bottomnav`** menyediakan ruang bawah untuk bilah itu. Tanpanya baris terakhir
78
+ setiap daftar di setiap layar berakhir di bawah bilah — dan baris yang tak bisa digulir ke sana
79
+ adalah baris yang tak seorang pun tahu ada.
80
+ - **`.fdy-app--navtoggle-end`** memindahkan tombol nav ke sisi jauh topbar. Ini ergonomi, bukan
81
+ selera: pada HP besar yang dipegang satu tangan, pojok kiri-ATAS adalah tempat tersulit dijangkau
82
+ ibu jari kanan, dan tombol itu yang paling sering ditekan di sana. Opt-in, sebab shell desktop yang
83
+ juga menaruh avatar di kanan punya persoalan sebaliknya.
84
+
85
+ ### Fixed
86
+ - **`.fdy-app__backdrop` kini membawa `cursor:pointer`** (`#057`). Ini bukan untuk tetikus. Safari
87
+ iOS hanya mensintesis `click` dari tap pada elemen yang dianggapnya interaktif, dan bentuk kursor
88
+ salah satu penanda yang dipakainya. Backdrop-nya `<div>` polos dengan handler klik — tanpa button,
89
+ tanpa role, tanpa tabindex — sehingga tanpa aturan ini sebuah tap di atasnya sama sekali tidak
90
+ menghasilkan klik: drawer tetap terbuka, scrim tetap gelap, dan memuat ulang halaman adalah
91
+ satu-satunya jalan keluar. Tidak dapat direproduksi di Chromium, baik dengan klik tetikus maupun
92
+ `touchscreen.tap()` — itu sebabnya tidak satu pun uji di kit ini maupun di app yang memakainya
93
+ pernah menangkapnya. Dijaga `browser/bottom-nav.mjs`.
94
+
12
95
  ## [3.3.1] - 2026-09-07
13
96
 
14
97
  **Tautan lewati yang tak punya tujuan, di tiga stack sekaligus.** `#056` (dari `keamanan-cluster`)
package/COMPONENTS.md CHANGED
@@ -146,7 +146,16 @@ off-canvas drawer and backdrop are built in.
146
146
  overriding `width` on `.fdy-app__sidebar`, whose value also feeds the collapse transition.
147
147
  - Modifiers: `--nav-open` (drawer open) · `--nav-collapsed` (collapse to zero width, ≥721px) ·
148
148
  `--nav-overlay` (≥721px: float the nav over the content instead of displacing it) · `--static`
149
- (embed the shell in a page instead of filling the viewport)
149
+ (embed the shell in a page instead of filling the viewport) · `--navtoggle-end` (move the nav
150
+ toggle to the far side of the topbar) · `--has-bottomnav` (reserve room for `.fdy-bottomnav`)
151
+ - **`--navtoggle-end`** is ergonomics, not taste: on a large phone held in one hand the top-LEFT
152
+ corner is the hardest place on the screen for a right thumb to reach, and the toggle is the
153
+ control pressed there most. Opt-in, because a desktop shell that also puts an avatar on the right
154
+ has the opposite problem.
155
+ - The backdrop carries `cursor:pointer`. That is **not** for the mouse: Safari on iOS only
156
+ synthesises a `click` from a tap on elements it treats as interactive, and the cursor is one of
157
+ the signals it uses. Without it, tapping outside the drawer on an iPhone does nothing at all —
158
+ the drawer stays open, the scrim stays dark, and reloading is the only way out.
150
159
 
151
160
  **`--nav-overlay` is a layout choice, not a second behaviour.** With it the nav floats at *every*
152
161
  width, so the drawer's whole code path — backdrop, Escape, close-on-follow, focus into the panel and
@@ -304,10 +313,33 @@ Responsive display: `.fdy-hide-below-sm|md|lg` · `.fdy-hide-above-sm|md|lg`.
304
313
  Breakpoints (min-width): `sm` 600 · `md` 960 · `lg` 1280 · `xl` 1920, importable as
305
314
  `import { breakpoints } from '@cahyo-dimas/freeday/breakpoints'`.
306
315
 
307
- **`breakpoints.nav` (721) is separate and is the one the shell uses.** `.fdy-app` switches the
308
- sidebar from off-canvas drawer to static column at 721px, *not* at `md`. Any `matchMedia` guard or
309
- utility variant that has to agree with the shell must use `nav`; using `md` leaves 721–959px broken
310
- (sidebar already static while your script still treats it as an overlay).
316
+ **Two widths in that object are not ramp steps**, and each is the one to use when your rule has to
317
+ fire with a kit rule. Aligning either to the nearest ramp step leaves a band of broken widths, in
318
+ which the kit has already switched and your app has not:
319
+
320
+ | | px | Governs | Aligning to the ramp instead breaks |
321
+ |---|---|---|---|
322
+ | `breakpoints.nav` | 721 | `.fdy-app` switches the sidebar from off-canvas drawer to static column — and `.fdy-app__main`'s padding step and `.fdy-bottomnav` ride the same width | 721–959px, if you use `md`: sidebar already static while your script still treats it as an overlay |
323
+ | `breakpoints.filterbar` | 640 | `.fdy-filterbar` stacks every field full-width | 600–640px, if you use `sm`: fields already stacked while your rule still caps the `--w-grow` one, so it sits visibly narrower than the fields under it |
324
+
325
+ **In CSS, import the breakpoints instead of retyping them.** `@media` cannot read a custom property,
326
+ so there is no `--breakpoint-*` token and never will be; `@cahyo-dimas/freeday/media` answers it the
327
+ only way CSS can, as a `@custom-media` sheet:
328
+
329
+ ```css
330
+ @import '@cahyo-dimas/freeday/media'; /* once, above the rules that use it */
331
+
332
+ @media (--fdy-filterbar-stacked) { .page-filters > .fdy-field--w-grow { max-width: none } }
333
+ @media (--fdy-nav-drawer) { .page-banner { padding-inline: var(--space-5) } }
334
+ ```
335
+
336
+ Declared: `--fdy-below-sm|md|lg|xl` and `--fdy-from-sm|md|lg|xl` (the ramp, matching the utilities
337
+ above), plus `--fdy-nav-drawer` / `--fdy-nav-static` and `--fdy-filterbar-stacked` for the two
338
+ widths in the table. It is **opt-in**: `@custom-media` is a PostCSS transform
339
+ (`postcss-custom-media`, included in `postcss-preset-env`), so an app that runs PostCSS — anything on
340
+ Vite already does — gets it, and an app that does not keeps writing literals exactly as before. The
341
+ sheet carries no rules, so importing it costs nothing at runtime, and it is deliberately not part of
342
+ `freeday.css` or `freeday.bundle.css`.
311
343
 
312
344
  **Visual style: `data-style="soft | glass"`.** `soft` is the default and the look the kit has always
313
345
  had; `glass` frosts the **raised** surfaces — card, modal, drawer, menu, appbar, the app shell's
@@ -1496,6 +1528,39 @@ Size-matched placeholders so nothing shifts when data lands: `--title` `--text`
1496
1528
 
1497
1529
  # Navigation
1498
1530
 
1531
+ ## Bottom navigation — `.fdy-bottomnav`
1532
+ The bar a **phone** application puts its three-to-five most-used destinations on, where a thumb
1533
+ reaches them. Shown only at `max-width:720px`; on a desktop the sidebar already carries the same
1534
+ links, and a second copy would be duplication.
1535
+
1536
+ Not part of `.fdy-app`'s markup: it is `position:fixed`, so it works from anywhere inside the
1537
+ shell, and an application that does not want one pays nothing. Put **`.fdy-app--has-bottomnav`** on
1538
+ the shell to reserve room for it — without that, the last row of every list ends underneath the bar,
1539
+ and a row nobody can scroll to is a row nobody knows is there.
1540
+
1541
+ - Items are `<a class="fdy-bottomnav__item">` with `__icon` / `__label`; the current one gets
1542
+ `aria-current="page"` — the same contract `.fdy-nav__item` uses, so "you are here" has one
1543
+ meaning in this kit, not two.
1544
+ - Targets are at least 44px tall. Under that a tap becomes a retry, and this bar is pressed more
1545
+ than any other control in a phone application.
1546
+ - Labels are **not** optional. An icon-only bar asks every user to have already learned what each
1547
+ glyph means. Long labels ellipsis rather than wrap, so the bar keeps one height.
1548
+ - Bottom padding includes `env(safe-area-inset-bottom)`, so labels do not sit under the home
1549
+ indicator on a notched phone.
1550
+
1551
+ ```html
1552
+ <nav class="fdy-bottomnav" aria-label="Main">
1553
+ <a class="fdy-bottomnav__item" href="/dashboard" aria-current="page">
1554
+ <span class="fdy-bottomnav__icon"><!--svg--></span>
1555
+ <span class="fdy-bottomnav__label">Dashboard</span>
1556
+ </a>
1557
+ <a class="fdy-bottomnav__item" href="/schedule">
1558
+ <span class="fdy-bottomnav__icon"><!--svg--></span>
1559
+ <span class="fdy-bottomnav__label">Schedule</span>
1560
+ </a>
1561
+ </nav>
1562
+ ```
1563
+
1499
1564
  ## Nav (menu) — `.fdy-nav`
1500
1565
  Navigation links, **vertical by default** (the app shell sidebar), horizontal with `--horizontal`. Items are `<a class="fdy-nav__item">` with
1501
1566
  `__icon` / `__label` / `__badge`; the current one gets `aria-current="page"`.
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.3.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.3.1)
8
+ [![Release](https://img.shields.io/badge/release-v3.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
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`.
@@ -60,7 +60,7 @@ npm i @cahyo-dimas/freeday
60
60
  ```js
61
61
  import '@cahyo-dimas/freeday/css'; // token + komponen (satu file)
62
62
  import '@cahyo-dimas/freeday'; // semua enhancer JS (auto-init [data-fdy-*])
63
- // granular bila perlu: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<nama>'
63
+ // granular bila perlu: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<nama>' · '@cahyo-dimas/freeday/media' (breakpoint @custom-media)
64
64
  ```
65
65
  Set tema di root app: `<html data-theme="light" data-density="comfortable">`. `dist/` di-commit &
66
66
  ter-publish → install tanpa build step; minify diserahkan ke bundler konsumen. Karena terbit di
@@ -153,9 +153,13 @@ container-nya, bukan scroll sendiri.
153
153
  menyusut/melebar saat `data-density` berubah.
154
154
  - (roadmap) `data-style` untuk varian visual lain.
155
155
  - Skala breakpoint (`sm`/`md`/`lg`/`xl` = 600/960/1280/1920px, sama dengan utilitas
156
- `src/components/breakpoints.css`) juga tersedia di JS: `import { breakpoints } from
157
- '@cahyo-dimas/freeday/breakpoints'`. Pakai itu untuk menyamakan `matchMedia`/`@media` app-mu
158
- dengan skala Freeday.
156
+ `src/components/breakpoints.css`) tersedia untuk app-mu dalam dua bahasa: `import { breakpoints }
157
+ from '@cahyo-dimas/freeday/breakpoints'` untuk JS (`matchMedia`, varian utility), dan `@import
158
+ '@cahyo-dimas/freeday/media'` untuk CSS, yang mendeklarasikan tiap lebar sebagai `@custom-media`
159
+ sehingga sebuah rule ditulis `@media (--fdy-below-sm)`, bukan literal yang diketik ulang (butuh
160
+ PostCSS; opt-in). Objeknya juga membawa dua lebar yang **bukan** anak tangga ramp dan tak boleh
161
+ dibulatkan ke salah satunya: `nav` (721, saklar drawer↔sidebar milik shell) dan `filterbar` (640,
162
+ tempat `.fdy-filterbar` menumpuk). Lihat [`COMPONENTS.md`](COMPONENTS.md) untuk apa yang diaturnya.
159
163
 
160
164
  ## Integrasi framework (SPA)
161
165
  > **Peta library lengkap:** [`docs/integrations.md`](docs/integrations.md). Tiap area
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.3.1-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.3.1)
8
+ [![Release](https://img.shields.io/badge/release-v3.5.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.5.0)
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`.
@@ -61,7 +61,7 @@ npm i @cahyo-dimas/freeday
61
61
  ```js
62
62
  import '@cahyo-dimas/freeday/css'; // tokens + components (single file)
63
63
  import '@cahyo-dimas/freeday'; // all JS enhancers (auto-init [data-fdy-*])
64
- // granular if needed: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<name>'
64
+ // granular if needed: '@cahyo-dimas/freeday/tokens' · '@cahyo-dimas/freeday/css/components' · '@cahyo-dimas/freeday/enhancers/<name>' · '@cahyo-dimas/freeday/media' (@custom-media breakpoints)
65
65
  ```
66
66
  Set the theme on your app root: `<html data-theme="light" data-density="comfortable">`. `dist/` is
67
67
  committed and published, so install needs **no build step**; minification is left to the consumer's
@@ -154,9 +154,13 @@ of scrolling.
154
154
  to shrink or grow when `data-density` changes.
155
155
  - (roadmap) `data-style` for alternative visual variants.
156
156
  - The breakpoint scale (`sm`/`md`/`lg`/`xl` = 600/960/1280/1920px, matching the
157
- `src/components/breakpoints.css` utilities) is also available in JS: `import { breakpoints } from
158
- '@cahyo-dimas/freeday/breakpoints'`. Use it to keep your app's `matchMedia`/`@media` on the same
159
- scale as Freeday's.
157
+ `src/components/breakpoints.css` utilities) is available to your app in both languages: `import
158
+ { breakpoints } from '@cahyo-dimas/freeday/breakpoints'` for JS (`matchMedia`, utility variants),
159
+ and `@import '@cahyo-dimas/freeday/media'` for CSS, which declares each one as a `@custom-media`
160
+ so a rule reads `@media (--fdy-below-sm)` instead of a retyped literal (needs PostCSS; opt-in).
161
+ The object also carries the two widths that are **not** ramp steps and must not be rounded to one:
162
+ `nav` (721, the shell's drawer↔sidebar switch) and `filterbar` (640, where `.fdy-filterbar`
163
+ stacks). See [`COMPONENTS.md`](COMPONENTS.md) for what each governs.
160
164
 
161
165
  ## Framework integration (SPA)
162
166
  > **Full library map:** [`docs/integrations.md`](docs/integrations.md). Each area (forms/validation,
@@ -1040,6 +1040,13 @@ a { color: var(--color-primary); }
1040
1040
  .fdy-app__navtoggle:hover{background:var(--color-surface-2);color:var(--color-text);}
1041
1041
  .fdy-app__navtoggle:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
1042
1042
  .fdy-app__navtoggle svg{display:block;width:1.25rem;height:1.25rem;}
1043
+ /* --navtoggle-end moves the toggle to the far side of the topbar. The title already
1044
+ * carries `margin-right:auto`, so ordering the toggle last is enough; nothing else needs
1045
+ * to know. On a large phone held in one hand the top-LEFT corner is the hardest place on
1046
+ * the screen for a right thumb to reach, and the toggle is the control most often pressed
1047
+ * there — so this is ergonomics, not taste. Left as an opt-in because a desktop shell that
1048
+ * also puts an avatar on the right has the opposite problem. */
1049
+ .fdy-app--navtoggle-end .fdy-app__navtoggle{order:1;}
1043
1050
  /* Desktop collapse: animate width -> 0 (content clips via overflow:hidden; the content area
1044
1051
  * flexes in to fill). Not display:none, so the collapse/expand slides instead of snapping. */
1045
1052
  @media (min-width:721px){.fdy-app--nav-collapsed .fdy-app__sidebar{width:0;border-right-width:0;}}
@@ -1075,7 +1082,15 @@ a { color: var(--color-primary); }
1075
1082
  * the initial `visible`, so a mode switch alone hands the page ~200ms of dead clicks. Measured: with
1076
1083
  * only `visibility:hidden`, elementFromPoint over the nav toggle returned the backdrop.
1077
1084
  * Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
1078
- .fdy-app__backdrop{display:none;}
1085
+ /* `cursor:pointer` here is not decoration, and not for the mouse. Safari on iOS only
1086
+ * synthesises a `click` from a tap on elements it treats as interactive, and the cursor
1087
+ * being a pointer is one of the signals it uses. The backdrop is a plain <div> with a
1088
+ * click handler — no button, no role, no tabindex — so without this rule a tap on it
1089
+ * produces no click at all: the drawer stays open, the scrim stays dark, and the only way
1090
+ * out is reloading the page. Reported from a real iPhone X; not reproducible in Chromium
1091
+ * with either mouse click or `touchscreen.tap()`, which is why no test in this kit or in
1092
+ * the consuming app had ever caught it. */
1093
+ .fdy-app__backdrop{display:none;cursor:pointer;}
1079
1094
  @media (max-width:720px){
1080
1095
  .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);transition:transform var(--dur-base) var(--ease-standard);box-shadow:var(--shadow-4);}
1081
1096
  .fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
@@ -1200,6 +1215,69 @@ a { color: var(--color-primary); }
1200
1215
  .fdy-badge-ov--bottom-right{top:auto;bottom:0;transform:translate(50%,50%);}
1201
1216
  .fdy-badge-ov--bottom-left{top:auto;bottom:0;right:auto;left:0;transform:translate(-50%,50%);}
1202
1217
 
1218
+ /* Freeday. Bottom navigation
1219
+ *
1220
+ * The bar a phone application puts its three-to-five most-used destinations on. The kit
1221
+ * had `.fdy-nav--horizontal` for a top-nav application and the off-canvas drawer for a
1222
+ * sidebar one, and nothing for the arrangement almost every phone application actually
1223
+ * uses — which was only noticed when an app shipped to real phones and its owner reached
1224
+ * for a thumb-reachable bar that did not exist.
1225
+ *
1226
+ * Deliberately NOT part of `.fdy-app`'s markup: it is `position:fixed`, so it works from
1227
+ * anywhere inside the shell's content slot, and an application that does not want one
1228
+ * pays nothing. `.fdy-app--has-bottomnav` on the shell is what reserves room for it. */
1229
+ .fdy-bottomnav{display:none;}
1230
+ @media (max-width:720px){
1231
+ .fdy-bottomnav{
1232
+ display:flex;position:fixed;left:0;right:0;bottom:0;z-index:50;
1233
+ align-items:stretch;justify-content:space-around;gap:var(--space-1);
1234
+ padding:var(--space-2) var(--space-2);
1235
+ /* The home indicator on a notched phone sits over the bottom of the viewport, and a
1236
+ * bar that ignores it puts its labels underneath a physical UI element. Added to the
1237
+ * padding rather than substituted for it, so a phone without one is unaffected. */
1238
+ padding-bottom:calc(var(--space-2) + env(safe-area-inset-bottom,0px));
1239
+ background:var(--color-surface-raised);
1240
+ backdrop-filter:var(--surface-filter);
1241
+ -webkit-backdrop-filter:var(--surface-filter);
1242
+ border-top:var(--bw) solid var(--color-border);
1243
+ }
1244
+ /* Room for the bar, or the last row of every list on every screen sits under it -- and
1245
+ a row you cannot scroll to is a row nobody knows is there. */
1246
+ .fdy-app--has-bottomnav .fdy-app__main{
1247
+ padding-bottom:calc(var(--space-20) + env(safe-area-inset-bottom,0px));
1248
+ }
1249
+ }
1250
+ .fdy-bottomnav__item{
1251
+ display:flex;flex:1 1 0;min-width:0;flex-direction:column;align-items:center;
1252
+ justify-content:center;gap:2px;
1253
+ /* 44px is the smallest target a thumb hits reliably; anything under it turns a tap into
1254
+ * a retry, and this bar is pressed more than any other control in the application. */
1255
+ min-height:2.75rem;padding:var(--space-1);
1256
+ border-radius:var(--radius-md);
1257
+ font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-medium);
1258
+ color:var(--color-text-muted);text-decoration:none;text-align:center;
1259
+ transition:background var(--dur-fast) var(--ease-standard),
1260
+ color var(--dur-fast) var(--ease-standard);
1261
+ }
1262
+ .fdy-bottomnav__item:hover{color:var(--color-text);}
1263
+ .fdy-bottomnav__item:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
1264
+ /* The current destination is marked by `aria-current="page"`, the same attribute
1265
+ * `.fdy-nav__item` uses -- one contract for "you are here", not two. */
1266
+ .fdy-bottomnav__item[aria-current="page"]{
1267
+ color:var(--color-primary-strong);background:var(--color-primary-soft);
1268
+ }
1269
+ .fdy-bottomnav__icon{display:block;width:1.25rem;height:1.25rem;flex:none;}
1270
+ .fdy-bottomnav__icon svg{display:block;width:100%;height:100%;}
1271
+ /* Labels are never dropped in favour of icons alone: an icon-only bar asks every user to
1272
+ * have already learned what each glyph means, and the people using this kit's applications
1273
+ * on a phone are often using them for the first time. Long labels ellipsis rather than
1274
+ * wrap, so the bar keeps one height. */
1275
+ .fdy-bottomnav__label{
1276
+ display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
1277
+ line-height:1.2;
1278
+ }
1279
+ @media (prefers-reduced-motion:reduce){.fdy-bottomnav__item{transition:none;}}
1280
+
1203
1281
  /* Freeday. Breadcrumb */
1204
1282
  .fdy-breadcrumb{font-size:var(--text-sm);}
1205
1283
  .fdy-breadcrumb__list{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);list-style:none;margin:0;padding:0;}
@@ -1212,8 +1290,14 @@ a { color: var(--color-primary); }
1212
1290
 
1213
1291
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
1214
1292
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
1293
+ * Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
1294
+ * static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
1295
+ * in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
1296
+ * nearest ramp step, which is a band of broken widths in both directions.
1215
1297
  * NOTE: media-query breakpoints must be literal. CSS can't read custom properties
1216
- * inside @media, so these px are structural, not stray magic numbers. */
1298
+ * inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
1299
+ * need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
1300
+ * @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
1217
1301
  .fdy-hidden{display:none !important;}
1218
1302
 
1219
1303
  /* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
@@ -1999,7 +2083,14 @@ a { color: var(--color-primary); }
1999
2083
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
2000
2084
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
2001
2085
  .fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
2002
- /* Stack on narrow screens so nothing overflows the row. */
2086
+ /* Stack on narrow screens so nothing overflows the row.
2087
+ * 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
2088
+ * it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
2089
+ * in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
2090
+ * that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
2091
+ * exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
2092
+ * fields are already full width while the app still caps the growing one. Raised from
2093
+ * IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
2003
2094
  @media (max-width:640px){
2004
2095
  .fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
2005
2096
  .fdy-filterbar__actions{width:100%;margin-left:0;}
package/dist/freeday.css CHANGED
@@ -162,6 +162,13 @@ a { color: var(--color-primary); }
162
162
  .fdy-app__navtoggle:hover{background:var(--color-surface-2);color:var(--color-text);}
163
163
  .fdy-app__navtoggle:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
164
164
  .fdy-app__navtoggle svg{display:block;width:1.25rem;height:1.25rem;}
165
+ /* --navtoggle-end moves the toggle to the far side of the topbar. The title already
166
+ * carries `margin-right:auto`, so ordering the toggle last is enough; nothing else needs
167
+ * to know. On a large phone held in one hand the top-LEFT corner is the hardest place on
168
+ * the screen for a right thumb to reach, and the toggle is the control most often pressed
169
+ * there — so this is ergonomics, not taste. Left as an opt-in because a desktop shell that
170
+ * also puts an avatar on the right has the opposite problem. */
171
+ .fdy-app--navtoggle-end .fdy-app__navtoggle{order:1;}
165
172
  /* Desktop collapse: animate width -> 0 (content clips via overflow:hidden; the content area
166
173
  * flexes in to fill). Not display:none, so the collapse/expand slides instead of snapping. */
167
174
  @media (min-width:721px){.fdy-app--nav-collapsed .fdy-app__sidebar{width:0;border-right-width:0;}}
@@ -197,7 +204,15 @@ a { color: var(--color-primary); }
197
204
  * the initial `visible`, so a mode switch alone hands the page ~200ms of dead clicks. Measured: with
198
205
  * only `visibility:hidden`, elementFromPoint over the nav toggle returned the backdrop.
199
206
  * Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
200
- .fdy-app__backdrop{display:none;}
207
+ /* `cursor:pointer` here is not decoration, and not for the mouse. Safari on iOS only
208
+ * synthesises a `click` from a tap on elements it treats as interactive, and the cursor
209
+ * being a pointer is one of the signals it uses. The backdrop is a plain <div> with a
210
+ * click handler — no button, no role, no tabindex — so without this rule a tap on it
211
+ * produces no click at all: the drawer stays open, the scrim stays dark, and the only way
212
+ * out is reloading the page. Reported from a real iPhone X; not reproducible in Chromium
213
+ * with either mouse click or `touchscreen.tap()`, which is why no test in this kit or in
214
+ * the consuming app had ever caught it. */
215
+ .fdy-app__backdrop{display:none;cursor:pointer;}
201
216
  @media (max-width:720px){
202
217
  .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);transition:transform var(--dur-base) var(--ease-standard);box-shadow:var(--shadow-4);}
203
218
  .fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
@@ -322,6 +337,69 @@ a { color: var(--color-primary); }
322
337
  .fdy-badge-ov--bottom-right{top:auto;bottom:0;transform:translate(50%,50%);}
323
338
  .fdy-badge-ov--bottom-left{top:auto;bottom:0;right:auto;left:0;transform:translate(-50%,50%);}
324
339
 
340
+ /* Freeday. Bottom navigation
341
+ *
342
+ * The bar a phone application puts its three-to-five most-used destinations on. The kit
343
+ * had `.fdy-nav--horizontal` for a top-nav application and the off-canvas drawer for a
344
+ * sidebar one, and nothing for the arrangement almost every phone application actually
345
+ * uses — which was only noticed when an app shipped to real phones and its owner reached
346
+ * for a thumb-reachable bar that did not exist.
347
+ *
348
+ * Deliberately NOT part of `.fdy-app`'s markup: it is `position:fixed`, so it works from
349
+ * anywhere inside the shell's content slot, and an application that does not want one
350
+ * pays nothing. `.fdy-app--has-bottomnav` on the shell is what reserves room for it. */
351
+ .fdy-bottomnav{display:none;}
352
+ @media (max-width:720px){
353
+ .fdy-bottomnav{
354
+ display:flex;position:fixed;left:0;right:0;bottom:0;z-index:50;
355
+ align-items:stretch;justify-content:space-around;gap:var(--space-1);
356
+ padding:var(--space-2) var(--space-2);
357
+ /* The home indicator on a notched phone sits over the bottom of the viewport, and a
358
+ * bar that ignores it puts its labels underneath a physical UI element. Added to the
359
+ * padding rather than substituted for it, so a phone without one is unaffected. */
360
+ padding-bottom:calc(var(--space-2) + env(safe-area-inset-bottom,0px));
361
+ background:var(--color-surface-raised);
362
+ backdrop-filter:var(--surface-filter);
363
+ -webkit-backdrop-filter:var(--surface-filter);
364
+ border-top:var(--bw) solid var(--color-border);
365
+ }
366
+ /* Room for the bar, or the last row of every list on every screen sits under it -- and
367
+ a row you cannot scroll to is a row nobody knows is there. */
368
+ .fdy-app--has-bottomnav .fdy-app__main{
369
+ padding-bottom:calc(var(--space-20) + env(safe-area-inset-bottom,0px));
370
+ }
371
+ }
372
+ .fdy-bottomnav__item{
373
+ display:flex;flex:1 1 0;min-width:0;flex-direction:column;align-items:center;
374
+ justify-content:center;gap:2px;
375
+ /* 44px is the smallest target a thumb hits reliably; anything under it turns a tap into
376
+ * a retry, and this bar is pressed more than any other control in the application. */
377
+ min-height:2.75rem;padding:var(--space-1);
378
+ border-radius:var(--radius-md);
379
+ font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-medium);
380
+ color:var(--color-text-muted);text-decoration:none;text-align:center;
381
+ transition:background var(--dur-fast) var(--ease-standard),
382
+ color var(--dur-fast) var(--ease-standard);
383
+ }
384
+ .fdy-bottomnav__item:hover{color:var(--color-text);}
385
+ .fdy-bottomnav__item:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
386
+ /* The current destination is marked by `aria-current="page"`, the same attribute
387
+ * `.fdy-nav__item` uses -- one contract for "you are here", not two. */
388
+ .fdy-bottomnav__item[aria-current="page"]{
389
+ color:var(--color-primary-strong);background:var(--color-primary-soft);
390
+ }
391
+ .fdy-bottomnav__icon{display:block;width:1.25rem;height:1.25rem;flex:none;}
392
+ .fdy-bottomnav__icon svg{display:block;width:100%;height:100%;}
393
+ /* Labels are never dropped in favour of icons alone: an icon-only bar asks every user to
394
+ * have already learned what each glyph means, and the people using this kit's applications
395
+ * on a phone are often using them for the first time. Long labels ellipsis rather than
396
+ * wrap, so the bar keeps one height. */
397
+ .fdy-bottomnav__label{
398
+ display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
399
+ line-height:1.2;
400
+ }
401
+ @media (prefers-reduced-motion:reduce){.fdy-bottomnav__item{transition:none;}}
402
+
325
403
  /* Freeday. Breadcrumb */
326
404
  .fdy-breadcrumb{font-size:var(--text-sm);}
327
405
  .fdy-breadcrumb__list{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);list-style:none;margin:0;padding:0;}
@@ -334,8 +412,14 @@ a { color: var(--color-primary); }
334
412
 
335
413
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
336
414
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
415
+ * Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
416
+ * static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
417
+ * in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
418
+ * nearest ramp step, which is a band of broken widths in both directions.
337
419
  * NOTE: media-query breakpoints must be literal. CSS can't read custom properties
338
- * inside @media, so these px are structural, not stray magic numbers. */
420
+ * inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
421
+ * need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
422
+ * @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
339
423
  .fdy-hidden{display:none !important;}
340
424
 
341
425
  /* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
@@ -1121,7 +1205,14 @@ a { color: var(--color-primary); }
1121
1205
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
1122
1206
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
1123
1207
  .fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
1124
- /* Stack on narrow screens so nothing overflows the row. */
1208
+ /* Stack on narrow screens so nothing overflows the row.
1209
+ * 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
1210
+ * it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
1211
+ * in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
1212
+ * that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
1213
+ * exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
1214
+ * fields are already full width while the app still caps the growing one. Raised from
1215
+ * IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
1125
1216
  @media (max-width:640px){
1126
1217
  .fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
1127
1218
  .fdy-filterbar__actions{width:100%;margin-left:0;}
@@ -0,0 +1,27 @@
1
+ /* Freeday breakpoints as @custom-media. GENERATED by tokens/build.mjs. Do not edit by hand.
2
+ *
3
+ * Requires postcss-custom-media (or any PostCSS setup that includes postcss-preset-env); without it
4
+ * a browser ignores the at-rules and every @media (--fdy-*) in your CSS never matches. Import it
5
+ * ONCE, above the rules that use it:
6
+ *
7
+ * @import '@cahyo-dimas/freeday/media';
8
+ * @media (--fdy-below-sm) { .page-header { display: block } }
9
+ *
10
+ * This file carries no rules of its own, so importing it costs nothing at runtime; it is not part
11
+ * of freeday.css or freeday.bundle.css. */
12
+ @custom-media --fdy-below-sm (max-width: 599.98px);
13
+ @custom-media --fdy-from-sm (min-width: 600px);
14
+ @custom-media --fdy-below-md (max-width: 959.98px);
15
+ @custom-media --fdy-from-md (min-width: 960px);
16
+ @custom-media --fdy-below-lg (max-width: 1279.98px);
17
+ @custom-media --fdy-from-lg (min-width: 1280px);
18
+ @custom-media --fdy-below-xl (max-width: 1919.98px);
19
+ @custom-media --fdy-from-xl (min-width: 1920px);
20
+
21
+ /* NOT ramp steps. Each mirrors one hard-wired rule in the kit, and a rule that must fire with it
22
+ * uses THIS condition, never the nearest ramp step: aligning shell-dependent code to --fdy-below-md
23
+ * leaves 721–959px broken, and aligning a filter-bar rule to --fdy-below-sm leaves 600–640px
24
+ * half-stacked. See tokens/breakpoints.mjs for what each one governs. */
25
+ @custom-media --fdy-nav-drawer (max-width: 720px);
26
+ @custom-media --fdy-nav-static (min-width: 721px);
27
+ @custom-media --fdy-filterbar-stacked (max-width: 640px);
@@ -96,6 +96,7 @@ Blazor). Then verify the agent can actually read those files. An agent that can'
96
96
  | `docs/agent-onboarding.md` | This file. |
97
97
  | `CHANGELOG.md` | **What changed between the version this project had and the one it has now.** Read it after every upgrade; each entry says what broke, what is new, and why. |
98
98
  | `dist/` | Built CSS + enhancers. **`freeday.bundle.css` = tokens + components** (what `@cahyo-dimas/freeday/css` resolves to); `freeday.css` is components **only**, `freeday.tokens.css` tokens only, so linking `freeday.css` alone leaves every `var(--…)` unresolved. Plus `freeday-*.js` and the `.d.ts` files. |
99
+ | `dist/freeday.media.css` | The breakpoints as `@custom-media` (`@cahyo-dimas/freeday/media`), so a responsive rule in your CSS states the kit's condition instead of a retyped literal. Needs PostCSS; carries no rules, and is **not** in the bundle. |
99
100
  | `src/components/*.css` | The authoritative source for every class, when a doc is ambiguous. |
100
101
  | `tokens/tokens.json` | Every token in W3C DTCG format, machine-readable. |
101
102
  | `adapters/vue` · `adapters/react` · `adapters/blazor` | Typed wrappers, 11 components each (plus `FdyTableFooter`). |
@@ -138,6 +139,7 @@ Recent additions most likely to replace something an app hand-rolled (all detail
138
139
  | `.fdy-field--full` | `max-width: none` on the form container, once per form, because the fields are a flex column and not a `.fdy-form-grid` |
139
140
  | `.fdy-nav--flat` | undoing the caret, the pointer cursor and the group divider yourself, on a sidebar whose group labels never collapse |
140
141
  | `--fdy-app-sidebar-w` | overriding `width` on `.fdy-app__sidebar`, which is also the rule that animates the collapse |
142
+ | `@media (--fdy-filterbar-stacked)` / `(--fdy-nav-drawer)`, from `@cahyo-dimas/freeday/media` | a literal `640px` or `720px` retyped into your stylesheet, which no longer matches the kit's rule the day the kit retunes it — and which you could only have found by reading `filterbar.css` |
141
143
 
142
144
  ## 3. Starting a new screen
143
145
 
@@ -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.3.1"` (public npm package). `dist/` is
194
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.5.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.1",
3
+ "version": "3.5.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,6 +54,10 @@
54
54
  "types": "./dist/asset.d.ts",
55
55
  "default": "./dist/freeday.tokens.css"
56
56
  },
57
+ "./media": {
58
+ "types": "./dist/asset.d.ts",
59
+ "default": "./dist/freeday.media.css"
60
+ },
57
61
  "./breakpoints": {
58
62
  "types": "./tokens/breakpoints.d.ts",
59
63
  "default": "./tokens/breakpoints.mjs"
@@ -92,7 +96,7 @@
92
96
  "scripts": {
93
97
  "build": "node tokens/build.mjs",
94
98
  "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/field-width.mjs browser/cfl-multi.mjs browser/cfl-dialog-only.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",
99
+ "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/cfl-dialog-only.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/bottom-nav.mjs browser/picker-states.mjs browser/tabs-strip.mjs browser/cfl-narrow.mjs browser/harness-contract.mjs",
96
100
  "prepack": "node tokens/build.mjs",
97
101
  "version": "node tokens/build.mjs && git add dist",
98
102
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
@@ -83,6 +83,13 @@
83
83
  .fdy-app__navtoggle:hover{background:var(--color-surface-2);color:var(--color-text);}
84
84
  .fdy-app__navtoggle:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
85
85
  .fdy-app__navtoggle svg{display:block;width:1.25rem;height:1.25rem;}
86
+ /* --navtoggle-end moves the toggle to the far side of the topbar. The title already
87
+ * carries `margin-right:auto`, so ordering the toggle last is enough; nothing else needs
88
+ * to know. On a large phone held in one hand the top-LEFT corner is the hardest place on
89
+ * the screen for a right thumb to reach, and the toggle is the control most often pressed
90
+ * there — so this is ergonomics, not taste. Left as an opt-in because a desktop shell that
91
+ * also puts an avatar on the right has the opposite problem. */
92
+ .fdy-app--navtoggle-end .fdy-app__navtoggle{order:1;}
86
93
  /* Desktop collapse: animate width -> 0 (content clips via overflow:hidden; the content area
87
94
  * flexes in to fill). Not display:none, so the collapse/expand slides instead of snapping. */
88
95
  @media (min-width:721px){.fdy-app--nav-collapsed .fdy-app__sidebar{width:0;border-right-width:0;}}
@@ -118,7 +125,15 @@
118
125
  * the initial `visible`, so a mode switch alone hands the page ~200ms of dead clicks. Measured: with
119
126
  * only `visibility:hidden`, elementFromPoint over the nav toggle returned the backdrop.
120
127
  * Mobile: the sidebar becomes an off-canvas drawer opened by the topbar toggle. */
121
- .fdy-app__backdrop{display:none;}
128
+ /* `cursor:pointer` here is not decoration, and not for the mouse. Safari on iOS only
129
+ * synthesises a `click` from a tap on elements it treats as interactive, and the cursor
130
+ * being a pointer is one of the signals it uses. The backdrop is a plain <div> with a
131
+ * click handler — no button, no role, no tabindex — so without this rule a tap on it
132
+ * produces no click at all: the drawer stays open, the scrim stays dark, and the only way
133
+ * out is reloading the page. Reported from a real iPhone X; not reproducible in Chromium
134
+ * with either mouse click or `touchscreen.tap()`, which is why no test in this kit or in
135
+ * the consuming app had ever caught it. */
136
+ .fdy-app__backdrop{display:none;cursor:pointer;}
122
137
  @media (max-width:720px){
123
138
  .fdy-app__sidebar{position:fixed;top:0;left:0;bottom:0;height:100dvh;width:16rem;z-index:60;transform:translateX(-100%);transition:transform var(--dur-base) var(--ease-standard);box-shadow:var(--shadow-4);}
124
139
  .fdy-app--nav-open .fdy-app__sidebar{transform:translateX(0);}
@@ -0,0 +1,62 @@
1
+ /* Freeday. Bottom navigation
2
+ *
3
+ * The bar a phone application puts its three-to-five most-used destinations on. The kit
4
+ * had `.fdy-nav--horizontal` for a top-nav application and the off-canvas drawer for a
5
+ * sidebar one, and nothing for the arrangement almost every phone application actually
6
+ * uses — which was only noticed when an app shipped to real phones and its owner reached
7
+ * for a thumb-reachable bar that did not exist.
8
+ *
9
+ * Deliberately NOT part of `.fdy-app`'s markup: it is `position:fixed`, so it works from
10
+ * anywhere inside the shell's content slot, and an application that does not want one
11
+ * pays nothing. `.fdy-app--has-bottomnav` on the shell is what reserves room for it. */
12
+ .fdy-bottomnav{display:none;}
13
+ @media (max-width:720px){
14
+ .fdy-bottomnav{
15
+ display:flex;position:fixed;left:0;right:0;bottom:0;z-index:50;
16
+ align-items:stretch;justify-content:space-around;gap:var(--space-1);
17
+ padding:var(--space-2) var(--space-2);
18
+ /* The home indicator on a notched phone sits over the bottom of the viewport, and a
19
+ * bar that ignores it puts its labels underneath a physical UI element. Added to the
20
+ * padding rather than substituted for it, so a phone without one is unaffected. */
21
+ padding-bottom:calc(var(--space-2) + env(safe-area-inset-bottom,0px));
22
+ background:var(--color-surface-raised);
23
+ backdrop-filter:var(--surface-filter);
24
+ -webkit-backdrop-filter:var(--surface-filter);
25
+ border-top:var(--bw) solid var(--color-border);
26
+ }
27
+ /* Room for the bar, or the last row of every list on every screen sits under it -- and
28
+ a row you cannot scroll to is a row nobody knows is there. */
29
+ .fdy-app--has-bottomnav .fdy-app__main{
30
+ padding-bottom:calc(var(--space-20) + env(safe-area-inset-bottom,0px));
31
+ }
32
+ }
33
+ .fdy-bottomnav__item{
34
+ display:flex;flex:1 1 0;min-width:0;flex-direction:column;align-items:center;
35
+ justify-content:center;gap:2px;
36
+ /* 44px is the smallest target a thumb hits reliably; anything under it turns a tap into
37
+ * a retry, and this bar is pressed more than any other control in the application. */
38
+ min-height:2.75rem;padding:var(--space-1);
39
+ border-radius:var(--radius-md);
40
+ font-family:var(--font-body);font-size:var(--text-xs);font-weight:var(--weight-medium);
41
+ color:var(--color-text-muted);text-decoration:none;text-align:center;
42
+ transition:background var(--dur-fast) var(--ease-standard),
43
+ color var(--dur-fast) var(--ease-standard);
44
+ }
45
+ .fdy-bottomnav__item:hover{color:var(--color-text);}
46
+ .fdy-bottomnav__item:focus-visible{outline:none;box-shadow:0 0 0 2px var(--color-primary);}
47
+ /* The current destination is marked by `aria-current="page"`, the same attribute
48
+ * `.fdy-nav__item` uses -- one contract for "you are here", not two. */
49
+ .fdy-bottomnav__item[aria-current="page"]{
50
+ color:var(--color-primary-strong);background:var(--color-primary-soft);
51
+ }
52
+ .fdy-bottomnav__icon{display:block;width:1.25rem;height:1.25rem;flex:none;}
53
+ .fdy-bottomnav__icon svg{display:block;width:100%;height:100%;}
54
+ /* Labels are never dropped in favour of icons alone: an icon-only bar asks every user to
55
+ * have already learned what each glyph means, and the people using this kit's applications
56
+ * on a phone are often using them for the first time. Long labels ellipsis rather than
57
+ * wrap, so the bar keeps one height. */
58
+ .fdy-bottomnav__label{
59
+ display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
60
+ line-height:1.2;
61
+ }
62
+ @media (prefers-reduced-motion:reduce){.fdy-bottomnav__item{transition:none;}}
@@ -1,7 +1,13 @@
1
1
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
2
2
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
3
+ * Two further widths exist and are NOT ramp steps: `nav` (app-shell.css, off-canvas drawer vs
4
+ * static sidebar) and `filterbar` (filterbar.css, the width its fields stack at). Both are named
5
+ * in tokens/breakpoints.mjs; a rule that has to fire with one of them aligns to it, not to the
6
+ * nearest ramp step, which is a band of broken widths in both directions.
3
7
  * NOTE: media-query breakpoints must be literal. CSS can't read custom properties
4
- * inside @media, so these px are structural, not stray magic numbers. */
8
+ * inside @media, so these px are structural, not stray magic numbers. An app that runs PostCSS
9
+ * need not retype them: `@cahyo-dimas/freeday/media` declares every width above as a
10
+ * @custom-media, so it can write `@media (--fdy-below-sm)` and never drift (#058 §2). */
5
11
  .fdy-hidden{display:none !important;}
6
12
 
7
13
  /* hide-below-X, hidden when the viewport is narrower than X (content shows at X and up) */
@@ -41,7 +41,14 @@
41
41
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge, so when the
42
42
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
43
43
  .fdy-filterbar--actions-inline .fdy-filterbar__actions{margin-left:0;}
44
- /* Stack on narrow screens so nothing overflows the row. */
44
+ /* Stack on narrow screens so nothing overflows the row.
45
+ * 640 is deliberately not a ramp step (sm is 600): it is a real phone-landscape width, and moving
46
+ * it now would reflow every filter bar in every consuming app. So it is NAMED instead — `filterbar`
47
+ * in tokens/breakpoints.mjs, `--fdy-filterbar-stacked` in the media sheet — because an app rule
48
+ * that must fire with the stack (releasing a --w-grow field's max-width, say) has to match this
49
+ * exact condition, and aligning it to sm from the docs alone leaves 600–640px half-stacked: the
50
+ * fields are already full width while the app still caps the growing one. Raised from
51
+ * IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1). */
45
52
  @media (max-width:640px){
46
53
  .fdy-filterbar>.fdy-field,.fdy-filterbar>.fdy-field--w-sm,.fdy-filterbar>.fdy-field--w-lg,.fdy-filterbar>.fdy-field--w-xl,.fdy-filterbar>.fdy-field--w-2xl,.fdy-filterbar>.fdy-field--w-grow{width:100%;flex:1 1 100%;}
47
54
  .fdy-filterbar__actions{width:100%;margin-left:0;}
@@ -4,6 +4,10 @@ export declare const breakpoints: {
4
4
  /** Width at which .fdy-app's sidebar turns from off-canvas drawer into a static column.
5
5
  * Not part of the sm/md/lg/xl ramp; it mirrors app-shell.css and nothing else. */
6
6
  nav: number;
7
+ /** Width at or below which .fdy-filterbar stacks every field full-width.
8
+ * Not part of the sm/md/lg/xl ramp; it mirrors filterbar.css and nothing else. A rule that
9
+ * must fire with the stack uses this, not `sm` — `sm` leaves 600–640px half-stacked. */
10
+ filterbar: number;
7
11
  sm: number;
8
12
  md: number;
9
13
  lg: number;
@@ -1,11 +1,25 @@
1
1
  // Freeday breakpoint scale in px, mirrors src/components/breakpoints.css utilities
2
2
  // (sm/md/lg/xl). CSS @media can't read custom properties, so this is the JS-side
3
- // source of the scale for matchMedia / app @media alignment.
3
+ // source of the scale for matchMedia / app @media alignment. The CSS-side answer to the
4
+ // same problem is dist/freeday.media.css (`@cahyo-dimas/freeday/media`), a @custom-media
5
+ // sheet GENERATED from this object, so the two halves cannot disagree.
4
6
  //
5
- // `nav` is not part of the sm/md/lg/xl ramp: it is the width at which .fdy-app switches the
6
- // sidebar from an off-canvas drawer (below) to a static column (at and above), hard-wired in
7
- // src/components/app-shell.css. Anything that has to agree with the shell, a matchMedia guard
8
- // deciding whether the nav toggle sets --nav-open or --nav-collapsed, a utility-framework
9
- // variant, must use THIS number, not md. Aligning such code to md (960) leaves 721–959px
10
- // broken: the sidebar is already static while the script still thinks it is an overlay.
11
- export const breakpoints = { nav: 721, sm: 600, md: 960, lg: 1280, xl: 1920 };
7
+ // `nav` and `filterbar` are not part of the sm/md/lg/xl ramp. Each mirrors one hard-wired rule in
8
+ // the kit, and each must be used INSTEAD of the nearest ramp step that is the whole reason they
9
+ // are named here rather than left to be discovered by reading the stylesheet.
10
+ //
11
+ // `nav` is the width at which .fdy-app switches the sidebar from an off-canvas drawer (below) to a
12
+ // static column (at and above), hard-wired in src/components/app-shell.css. Anything that has to
13
+ // agree with the shell, a matchMedia guard deciding whether the nav toggle sets --nav-open or
14
+ // --nav-collapsed, a utility-framework variant, must use THIS number, not md. Aligning such code to
15
+ // md (960) leaves 721–959px broken: the sidebar is already static while the script still thinks it
16
+ // is an overlay.
17
+ //
18
+ // `filterbar` is the width at which .fdy-filterbar stacks every field full-width, hard-wired in
19
+ // src/components/filterbar.css. It is deliberately not sm: 640 is a real phone-landscape width and
20
+ // moving it would reflow every filter bar in every consuming app. An app rule that has to fire in
21
+ // lockstep with the stack — releasing a growing field's max-width, say — must use THIS number.
22
+ // Aligning it to sm (600) leaves 600–640px broken: the kit has already stacked the fields while the
23
+ // app still caps one of them, so it sits visibly narrower than the fields under it. Raised from
24
+ // IDU_AI_DOC_SAPB1_CLIENT and IDU_AI_DOC_SAAS (#058 §1).
25
+ export const breakpoints = { nav: 721, filterbar: 640, sm: 600, md: 960, lg: 1280, xl: 1920 };