@cahyo-dimas/freeday 3.14.0 → 3.14.2

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,72 @@ 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.14.2] - 2026-10-30
13
+
14
+ **`useFreeday(ref)` dengan ref yang masih kosong kini bersuara.** Ref yang lupa dipasang di template
15
+ membuat adapter memindai seluruh dokumen tanpa tanda apa pun. Hasilnya bekerja, hanya lebih luas
16
+ dari yang diminta, dan markup lain di halaman diam-diam ikut bergantung padanya sampai hari ref itu
17
+ dipasang (`CATATAN-FREEDAY.md` §17). Kebalikan dari cakupan yang terlalu sempit di 3.13.0 `#099`.
18
+
19
+ ### Added
20
+ - **Vue dan React: satu `console.warn` per komponen ketika ref yang diberikan ke `useFreeday` masih
21
+ `null` saat mount** (`#105`). Di Vue diperiksa di `onMounted`, di React sesudah commit pertama;
22
+ pembaruan sesudahnya tidak mengulangnya. Pesannya menyebut dua penyebab (ref tak terpasang, atau
23
+ terpasang di elemen di balik `v-if`/kondisi) dan dua jalan keluarnya (pasang di elemen yang selalu
24
+ dirender, atau `useFreeday()` tanpa argumen bila seluruh dokumen memang yang dimaksud).
25
+
26
+ ### Catatan
27
+ - **Yang dipindai tidak berubah.** Ref kosong tetap jatuh ke seluruh dokumen. Adapter tidak dapat
28
+ membedakan ref yang lupa dari ref yang sekadar terlambat, dan mempersempitnya sekarang akan
29
+ mematikan dengan diam markup yang hari ini hanya hidup karena pemindaian itu, persis kelas cacat
30
+ `#099`. Peringatan ini memberi aplikasi waktu untuk membereskannya lebih dulu.
31
+ - Peringatan dinyalakan di setiap build, bukan hanya mode dev, sama seperti
32
+ `freeday-unhydrated.js`: kit tidak punya bendera build, dan peringatan ini hanya muncul pada kode
33
+ yang memang keliru.
34
+ - Blazor tidak dicakup: `ElementReference` adalah struct, bukan ref yang dapat `null`, dan apa yang
35
+ sampai ke `FreedayBlazor.initAll` dari `@ref` yang lupa ditangkap belum diukur.
36
+
37
+ ## [3.14.1] - 2026-10-30
38
+
39
+ **Badan `FdyModal` kembali setinggi isinya di iPhone.** Di iPhone 7 sungguhan (iOS 15), setiap modal
40
+ menampilkan kepala dan kaki dengan badan tinggal selarik, tergulir sampai dasar, dan formulirnya
41
+ tak terlihat. "Tambah security" dan "Tulis info" di app ronda tak dapat dipakai sama sekali dari HP
42
+ (`CATATAN-FREEDAY.md` §16).
43
+
44
+ ### Fixed
45
+ - **`.fdy-modal__body` memakai `flex:1 1 auto`, bukan `flex:1`** (`#104`). Basis shorthand itu 0%,
46
+ dan `.fdy-modal` hanya punya `max-height`, sehingga persentasenya diresolusikan terhadap tinggi
47
+ yang tak tentu. Mesin terkini membacanya sebagai `content`, sedangkan WebKit lama membacanya
48
+ sebagai 0: dialog setinggi kepala ditambah kaki, dan badan tinggal padding 40px dengan
49
+ formulirnya meluap di dalamnya. Basis `0px` mereproduksinya di Chrome sampai ke pikselnya (badan
50
+ 39,7px untuk isi 358px), dan `auto` menghasilkan tata letak yang identik dengan sebelumnya di mesin
51
+ terkini (dialog 498,7px, badan 355,7px, di kedua bacaan). Mekanismenya terukur. Bahwa Safari 15
52
+ memang membaca 0% sebagai 0 disimpulkan dari gejalanya dan belum diukur di perangkat, sebab tak
53
+ ada WebKit setua itu yang dapat dijalankan di sini.
54
+ - **`100vh` diikuti `100dvh`** di `max-height` modal, tinggi drawer, dan tinggi sidebar sticky
55
+ (`#104`). Di Safari iOS `100vh` adalah viewport besar, seolah bilah alat tak ada, jadi kotak yang
56
+ diukur dengannya berjalan ke bawah bilah itu. Sidebar overlay sudah memakai `100dvh` sejak lama;
57
+ tiga tempat ini tertinggal. Mesin tanpa `dvh` memakai nilai pertama.
58
+
59
+ ### Catatan
60
+ - Dugaan di catatan app, bahwa `100vh` penyebab utamanya, tidak cocok dengan gejalanya: `100vh` yang
61
+ kebesaran membuat dialog terlalu tinggi, bukan badannya terlalu pendek. Tetap diperbaiki karena
62
+ ia cacat tersendiri.
63
+ - Drawer dan nav sidebar memakai pola `flex:1` yang sama tetapi tidak terkena, sebab kontainernya
64
+ punya tinggi pasti.
65
+
66
+ ### Gerbang
67
+ - `css.test.mjs`: item flex yang menjadi wilayah gulir (`min-height:0` + `overflow`) tak boleh
68
+ memakai basis nol atau persen, kecuali kontainernya bertinggi pasti dan tercatat. Setiap `100vh`
69
+ yang mengukur kotak wajib diikuti `100dvh`, kecuali tinggi minimum halaman.
70
+ - `browser/modal-phone.mjs` di 375×667: badan modal pendek setinggi isinya, modal panjang berhenti
71
+ di `max-height` dengan kaki tetap di layar, dan basis terkomputasi `auto`. Dengan basis `0px`
72
+ uji ini merah dengan "40px shown of 358px", angka yang sama dengan di iPhone.
73
+ - `webkit/modal-phone.spec.mjs`: tata letak yang sama di WebKit dengan deskriptor iPhone 7 di CI. WebKit
74
+ Playwright adalah mesin terkini yang membaca 0% dengan benar, jadi berkas ini menjaga hasilnya,
75
+ bukan penyebabnya; penyebabnya dijaga dua gerbang di atas.
76
+ - Empat mutasi diverifikasi merah.
77
+
12
78
  ## [3.14.0] - 2026-10-29
13
79
 
14
80
  **Daftar terurut lewat seret dan papan ketik, dan satu atribut untuk berkata "ini bukan urusan
package/COMPONENTS.md CHANGED
@@ -52,6 +52,11 @@ live docs.
52
52
  never enhanced logs one `console.warn` naming it, with the element attached. The usual cause is
53
53
  a `useFreeday()` ref on a wrapper that does not contain it. `freeday-unhydrated.js` does this,
54
54
  and it ships in the root import.
55
+ **A `useFreeday(ref)` whose ref is still null at mount says so too.** That is a ref left off the
56
+ template, or one on an element behind `v-if` / a condition, and the call falls back to the whole
57
+ document: everything works, wider than asked, and other markup on the page can come to rely on
58
+ that scan unnoticed. It logs one `console.warn` per component. Put the ref on an element that is
59
+ always rendered, or write `useFreeday()` with no argument when the document is what you mean.
55
60
  **`data-fdy-ignore` says "not yours".** On an element or any ancestor, every enhancer skips that
56
61
  subtree, even under an explicit `initAll(document)`, and the warning above stays quiet. Use it
57
62
  where the app drives kit-styled markup itself, such as a chip matrix with its own removal flow.
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.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.2-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.2)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.2-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.2)
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`.
@@ -8,7 +8,8 @@ export interface UseFreedayReturn {
8
8
  /**
9
9
  * Auto-initialise Freeday enhancers over a component's subtree on mount and
10
10
  * after every commit. Pass a ref to scope the work; omit it to scan the whole
11
- * document.
11
+ * document. A ref that is still null after the first commit also scans the
12
+ * whole document, and warns once: bind it to an always-rendered element, or omit it.
12
13
  */
13
14
  export declare function useFreeday(rootRef?: RefObject<HTMLElement | null>): UseFreedayReturn;
14
15
 
@@ -14,10 +14,16 @@
14
14
  * well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
15
15
  * the other failed silently: querySelectorAll never matches its root.)
16
16
  *
17
+ * A ref that is still null after the first commit is a mistake, and an unusually quiet one: the
18
+ * call falls back to the whole document, so everything works, only wider than asked, and other code
19
+ * on the page can come to depend on that scan without knowing it. It warns once, after the first
20
+ * commit. Pass no argument, useFreeday(), when the whole document is what you mean; and put the ref
21
+ * on an element that is always rendered, not one behind a condition.
22
+ *
17
23
  * Events are native bubbling CustomEvents, add listeners on the root (they
18
24
  * bubble) and read event.detail. See index.d.ts for the detail types.
19
25
  */
20
- import { useEffect, useCallback } from 'react';
26
+ import { useEffect, useCallback, useRef } from 'react';
21
27
 
22
28
  // Discover Freeday enhancer namespaces once (all registered by the time an app
23
29
  // mounts, since `import 'freeday'` runs first). Each exposes initAll(el?).
@@ -36,7 +42,13 @@ function freedayApis() {
36
42
  return _apis;
37
43
  }
38
44
 
45
+ var EMPTY_REF = 'Freeday: useFreeday(ref) got a ref that is still null after the first commit, so it hydrated '
46
+ + 'the whole document instead of one subtree. Either ref={...} is missing from the JSX, or it sits on an '
47
+ + 'element that is not rendered yet (a condition). Put the ref on an element that is always rendered, or '
48
+ + 'call useFreeday() with no argument if the whole document is what you mean.';
49
+
39
50
  export function useFreeday(rootRef) {
51
+ var checked = useRef(false);
40
52
  var rehydrate = useCallback(function () {
41
53
  var el = rootRef && rootRef.current ? rootRef.current : undefined;
42
54
  var apis = freedayApis();
@@ -44,6 +56,12 @@ export function useFreeday(rootRef) {
44
56
  }, [rootRef]);
45
57
  // No dependency array: runs after every commit (mount + updates), mirroring
46
58
  // the Vue adapter's onMounted + onUpdated.
47
- useEffect(rehydrate);
59
+ useEffect(function () {
60
+ if (!checked.current) {
61
+ checked.current = true;
62
+ if (rootRef && !rootRef.current) console.warn(EMPTY_REF);
63
+ }
64
+ rehydrate();
65
+ });
48
66
  return { rehydrate: rehydrate };
49
67
  }
@@ -8,7 +8,8 @@ export interface UseFreedayReturn {
8
8
  /**
9
9
  * Auto-initialise Freeday enhancers over a component's subtree on mount and
10
10
  * after every update. Pass a template ref to scope the work; omit it to scan
11
- * the whole document.
11
+ * the whole document. A ref that is still null at mount also scans the whole
12
+ * document, and warns once: bind it to an always-rendered element, or omit it.
12
13
  */
13
14
  export declare function useFreeday(rootRef?: Ref<HTMLElement | null>): UseFreedayReturn;
14
15
 
@@ -15,6 +15,12 @@
15
15
  * well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
16
16
  * the other failed silently: querySelectorAll never matches its root.)
17
17
  *
18
+ * A ref that is still null once the component has mounted is a mistake, and an unusually quiet
19
+ * one: the call falls back to the whole document, so everything works, only wider than asked, and
20
+ * other code on the page can come to depend on that scan without knowing it. It warns once, at
21
+ * mount. Pass no argument, useFreeday(), when the whole document is what you mean; and put the ref
22
+ * on an element that is always rendered, not one behind v-if.
23
+ *
18
24
  * Events come straight through as native bubbling CustomEvents, bind them with
19
25
  * plain v-on (e.g. @fdy-cascade-change) and read event.detail. See index.d.ts
20
26
  * for the detail types.
@@ -36,6 +42,11 @@ function freedayApis() {
36
42
  return apis;
37
43
  }
38
44
 
45
+ var EMPTY_REF = 'Freeday: useFreeday(ref) got a ref that is still null after mount, so it hydrated the whole '
46
+ + 'document instead of one subtree. Either ref="..." is missing from the template, or it sits on an element '
47
+ + 'that is not rendered yet (v-if). Put the ref on an element that is always rendered, or call useFreeday() '
48
+ + 'with no argument if the whole document is what you mean.';
49
+
39
50
  export function useFreeday(rootRef) {
40
51
  var apis = null;
41
52
  function rehydrate() {
@@ -43,7 +54,10 @@ export function useFreeday(rootRef) {
43
54
  var el = rootRef && 'value' in rootRef ? (rootRef.value || undefined) : undefined;
44
55
  for (var i = 0; i < apis.length; i++) apis[i].initAll(el);
45
56
  }
46
- onMounted(rehydrate);
57
+ onMounted(function () {
58
+ if (rootRef && 'value' in rootRef && !rootRef.value) console.warn(EMPTY_REF);
59
+ rehydrate();
60
+ });
47
61
  onUpdated(function () { nextTick(rehydrate); });
48
62
  return { rehydrate: rehydrate };
49
63
  }
@@ -1285,7 +1285,8 @@ a { color: var(--color-primary-strong); }
1285
1285
  * > (.fdy-app__topbar, .fdy-app__main)). The __content wrapper gives the sticky
1286
1286
  * topbar a tall containing block so it can travel, a bare grid cell can't. */
1287
1287
  .fdy-app{display:flex;align-items:stretch;min-height:100vh;background:var(--color-surface-2);}
1288
- .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
1288
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
1289
+ .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;height:100dvh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
1289
1290
  /* `min-height:0` is the height-axis twin of `min-width:0`, and its absence was invisible for as long
1290
1291
  * as the shell scrolled the PAGE (#055 §2). A consumer that pins the shell to the viewport and
1291
1292
  * scrolls .fdy-app__main instead hits it immediately: a column flex item's automatic minimum size is
@@ -2472,7 +2473,8 @@ a { color: var(--color-primary-strong); }
2472
2473
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
2473
2474
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
2474
2475
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
2475
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
2476
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
2477
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;height:100dvh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
2476
2478
  .fdy-drawer[open]{display:flex;transform:none;}
2477
2479
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
2478
2480
  .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
@@ -2779,7 +2781,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2779
2781
  .fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
2780
2782
 
2781
2783
  /* Freeday. Modal (native <dialog>) */
2782
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
2784
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
2785
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));max-height:calc(100dvh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
2783
2786
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
2784
2787
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
2785
2788
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
@@ -2789,7 +2792,15 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
2789
2792
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
2790
2793
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
2791
2794
  .fdy-modal__close:hover{background:var(--color-surface-2);color:var(--color-text);}
2792
- .fdy-modal__body{flex:1;min-height:0;padding:var(--space-5);overflow:auto;}
2795
+ /* `1 1 auto`, NOT `flex:1`. The shorthand's basis is 0%, and `.fdy-modal` has only a max-height, so
2796
+ * the percentage resolves against an indefinite height. Current engines read that as `content`;
2797
+ * older WebKit read it as 0, sized the dialog to header + footer, and left the body its 40px of
2798
+ * padding, with the form overflowing inside it. That was every modal on an iPhone 7 (iOS 15),
2799
+ * reported from a real device (CATATAN-FREEDAY §16). A 0px basis reproduces it in Chrome to the
2800
+ * pixel. `auto` lays out identically in current engines (measured: 498.7px dialog, 355.7px body,
2801
+ * both readings) and leaves no indefinite percentage for any engine to read. min-height:0 is
2802
+ * still what lets a long body shrink and scroll once the dialog reaches its max-height. */
2803
+ .fdy-modal__body{flex:1 1 auto;min-height:0;padding:var(--space-5);overflow:auto;}
2793
2804
  .fdy-modal__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
2794
2805
  /* Symmetric enter AND exit (fade + rise + subtle scale) on the native <dialog>. @starting-style
2795
2806
  * animates the entry; overlay/display allow-discrete keep it painted through the exit so it
package/dist/freeday.css CHANGED
@@ -108,7 +108,8 @@ a { color: var(--color-primary-strong); }
108
108
  * > (.fdy-app__topbar, .fdy-app__main)). The __content wrapper gives the sticky
109
109
  * topbar a tall containing block so it can travel, a bare grid cell can't. */
110
110
  .fdy-app{display:flex;align-items:stretch;min-height:100vh;background:var(--color-surface-2);}
111
- .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
111
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
112
+ .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;height:100dvh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
112
113
  /* `min-height:0` is the height-axis twin of `min-width:0`, and its absence was invisible for as long
113
114
  * as the shell scrolled the PAGE (#055 §2). A consumer that pins the shell to the viewport and
114
115
  * scrolls .fdy-app__main instead hits it immediately: a column flex item's automatic minimum size is
@@ -1295,7 +1296,8 @@ a { color: var(--color-primary-strong); }
1295
1296
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
1296
1297
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
1297
1298
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
1298
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
1299
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
1300
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;height:100dvh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
1299
1301
  .fdy-drawer[open]{display:flex;transform:none;}
1300
1302
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
1301
1303
  .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
@@ -1602,7 +1604,8 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1602
1604
  .fdy-btn-split__toggle svg{display:block;width:1rem;height:1rem;}
1603
1605
 
1604
1606
  /* Freeday. Modal (native <dialog>) */
1605
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
1607
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
1608
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));max-height:calc(100dvh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
1606
1609
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
1607
1610
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
1608
1611
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
@@ -1612,7 +1615,15 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1612
1615
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
1613
1616
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
1614
1617
  .fdy-modal__close:hover{background:var(--color-surface-2);color:var(--color-text);}
1615
- .fdy-modal__body{flex:1;min-height:0;padding:var(--space-5);overflow:auto;}
1618
+ /* `1 1 auto`, NOT `flex:1`. The shorthand's basis is 0%, and `.fdy-modal` has only a max-height, so
1619
+ * the percentage resolves against an indefinite height. Current engines read that as `content`;
1620
+ * older WebKit read it as 0, sized the dialog to header + footer, and left the body its 40px of
1621
+ * padding, with the form overflowing inside it. That was every modal on an iPhone 7 (iOS 15),
1622
+ * reported from a real device (CATATAN-FREEDAY §16). A 0px basis reproduces it in Chrome to the
1623
+ * pixel. `auto` lays out identically in current engines (measured: 498.7px dialog, 355.7px body,
1624
+ * both readings) and leaves no indefinite percentage for any engine to read. min-height:0 is
1625
+ * still what lets a long body shrink and scroll once the dialog reaches its max-height. */
1626
+ .fdy-modal__body{flex:1 1 auto;min-height:0;padding:var(--space-5);overflow:auto;}
1616
1627
  .fdy-modal__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
1617
1628
  /* Symmetric enter AND exit (fade + rise + subtle scale) on the native <dialog>. @starting-style
1618
1629
  * animates the entry; overlay/display allow-discrete keep it painted through the exit so it
@@ -198,7 +198,7 @@ live docs also have a copy button per component.
198
198
  ```bash
199
199
  npm i @cahyo-dimas/freeday
200
200
  ```
201
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.14.0"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.14.2"` (public npm package). `dist/` is
202
202
  committed and published → no build step; `npm ci` runs without auth.
203
203
 
204
204
  ### 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.14.0",
3
+ "version": "3.14.2",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -96,7 +96,7 @@
96
96
  "scripts": {
97
97
  "build": "node tokens/build.mjs",
98
98
  "test": "node --test",
99
- "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs 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/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/picker-in-modal.mjs browser/harness-contract.mjs browser/app-owned.mjs browser/sortable.mjs",
99
+ "test:browser": "node --test --test-concurrency=3 browser/color-mix.mjs 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/tabs-vertical.mjs browser/menu-align.mjs browser/cfl-narrow.mjs browser/form-reveal.mjs browser/confirm-search-copy.mjs browser/daterange-settled.mjs browser/display-parts.mjs browser/table-empty.mjs browser/table-footer-text.mjs browser/month-mode.mjs browser/monthpicker.mjs browser/page-columns.mjs browser/table-hover.mjs browser/typed-popover.mjs browser/filter-parity.mjs browser/filter-wrap.mjs browser/filter-stack.mjs browser/timepicker.mjs browser/form-kit-required.mjs browser/picker-in-modal.mjs browser/harness-contract.mjs browser/app-owned.mjs browser/sortable.mjs browser/modal-phone.mjs browser/empty-ref.mjs",
100
100
  "prepack": "node tokens/build.mjs",
101
101
  "version": "node tokens/build.mjs && git add dist",
102
102
  "typecheck:react": "tsc -p adapters/react/tsconfig.json --noEmit",
@@ -6,7 +6,8 @@
6
6
  * > (.fdy-app__topbar, .fdy-app__main)). The __content wrapper gives the sticky
7
7
  * topbar a tall containing block so it can travel, a bare grid cell can't. */
8
8
  .fdy-app{display:flex;align-items:stretch;min-height:100vh;background:var(--color-surface-2);}
9
- .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
9
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
10
+ .fdy-app__sidebar{position:sticky;top:0;align-self:flex-start;height:100vh;height:100dvh;overflow:hidden;flex:none;width:var(--fdy-app-sidebar-w,15.5rem);box-sizing:border-box;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);border-right:var(--bw) solid var(--color-border);display:flex;flex-direction:column;transition:width var(--dur-slow) var(--ease-standard),border-right-width var(--dur-slow) var(--ease-standard);}
10
11
  /* `min-height:0` is the height-axis twin of `min-width:0`, and its absence was invisible for as long
11
12
  * as the shell scrolled the PAGE (#055 §2). A consumer that pins the shell to the viewport and
12
13
  * scrolls .fdy-app__main instead hits it immediately: a column flex item's automatic minimum size is
@@ -4,7 +4,8 @@
4
4
  /* display is gated behind [open] so the native <dialog> stays hidden until opened.
5
5
  * An unconditional display here would beat the UA `dialog:not([open]){display:none}`
6
6
  * rule (author origin wins over user-agent) and leave the drawer visible on load. */
7
- .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
7
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
8
+ .fdy-drawer{position:fixed;inset:0 auto 0 0;margin:0;padding:0;height:100vh;height:100dvh;max-height:none;width:min(20rem,82vw);flex-direction:column;border:0;background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-4),var(--surface-inset);overflow:hidden;transform:translateX(-100%);transition-property:transform,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
8
9
  .fdy-drawer[open]{display:flex;transform:none;}
9
10
  .fdy-drawer--right{inset:0 0 0 auto;transform:translateX(100%);}
10
11
  .fdy-drawer::backdrop{background:rgba(10,12,25,.5);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);opacity:0;transition-property:opacity,overlay,display;transition-duration:var(--dur-slow);transition-timing-function:var(--ease-standard),ease,ease;transition-behavior:normal,allow-discrete,allow-discrete;}
@@ -1,5 +1,6 @@
1
1
  /* Freeday. Modal (native <dialog>) */
2
- .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
2
+ /* 100vh then 100dvh: on iOS Safari 100vh is the LARGE viewport, as if the toolbar were gone, so a box sized by it runs under the toolbar. An engine without dvh keeps the first value. */
3
+ .fdy-modal{width:min(32rem,calc(100vw - var(--space-8)));max-height:calc(100vh - var(--space-16));max-height:calc(100dvh - var(--space-16));padding:0;border:var(--bw) solid var(--color-border);border-radius:var(--radius-xl);background:var(--color-surface-raised);backdrop-filter:var(--surface-filter);-webkit-backdrop-filter:var(--surface-filter);color:var(--color-text);box-shadow:var(--shadow-lift-hover),var(--surface-inset);overflow:hidden;flex-direction:column;opacity:0;transform:translateY(8px) scale(.98);/* Longhands, not the `transition` shorthand. A shorthand carrying one keyword the engine does not know is INVALID AT PARSE, so the whole declaration is thrown away -- and `allow-discrete` is unknown below Safari 17.4, which is under this kit's floor. The result was a modal and a drawer that opened and closed with no animation at all on an iPhone 7. Split this way only `transition-behavior` is dropped there, and the opacity/transform half survives. The lists align BY INDEX and reproduce the shorthand exactly, including `ease` for the discrete pair and `normal` for the continuous one -- verified by comparing computed styles before and after (#097). */transition-property:opacity,transform,overlay,display;transition-duration:var(--dur-base);transition-timing-function:var(--ease-spring),var(--ease-spring),ease,ease;transition-behavior:normal,normal,allow-discrete,allow-discrete;}
3
4
  .fdy-modal--sm{width:min(24rem,calc(100vw - var(--space-8)));}
4
5
  .fdy-modal--md{width:min(32rem,calc(100vw - var(--space-8)));}
5
6
  .fdy-modal--lg{width:min(48rem,calc(100vw - var(--space-8)));}
@@ -9,7 +10,15 @@
9
10
  .fdy-modal__title{font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--weight-semibold);color:var(--color-text);margin:0 auto 0 0;}
10
11
  .fdy-modal__close{border:0;background:transparent;color:var(--color-text-muted);font-size:var(--text-xl);line-height:1;cursor:pointer;padding:var(--space-1);border-radius:var(--radius-sm);}
11
12
  .fdy-modal__close:hover{background:var(--color-surface-2);color:var(--color-text);}
12
- .fdy-modal__body{flex:1;min-height:0;padding:var(--space-5);overflow:auto;}
13
+ /* `1 1 auto`, NOT `flex:1`. The shorthand's basis is 0%, and `.fdy-modal` has only a max-height, so
14
+ * the percentage resolves against an indefinite height. Current engines read that as `content`;
15
+ * older WebKit read it as 0, sized the dialog to header + footer, and left the body its 40px of
16
+ * padding, with the form overflowing inside it. That was every modal on an iPhone 7 (iOS 15),
17
+ * reported from a real device (CATATAN-FREEDAY §16). A 0px basis reproduces it in Chrome to the
18
+ * pixel. `auto` lays out identically in current engines (measured: 498.7px dialog, 355.7px body,
19
+ * both readings) and leaves no indefinite percentage for any engine to read. min-height:0 is
20
+ * still what lets a long body shrink and scroll once the dialog reaches its max-height. */
21
+ .fdy-modal__body{flex:1 1 auto;min-height:0;padding:var(--space-5);overflow:auto;}
13
22
  .fdy-modal__footer{flex:none;display:flex;gap:var(--space-2);justify-content:flex-end;padding:var(--space-4) var(--space-5);border-top:var(--bw) solid var(--color-border-muted);background:var(--color-surface-2);}
14
23
  /* Symmetric enter AND exit (fade + rise + subtle scale) on the native <dialog>. @starting-style
15
24
  * animates the entry; overlay/display allow-discrete keep it painted through the exit so it