@cahyo-dimas/freeday 3.9.4 → 3.11.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.
@@ -1213,9 +1213,6 @@ a { color: var(--color-primary-strong); }
1213
1213
  * framework with its preflight OFF (the recommended pairing), a semantic <ul>/<ol> keeps native
1214
1214
  * bullets + a 40px indent; add this class (or a Freeday list component) to normalise it. */
1215
1215
  .fdy-list-reset { list-style: none; margin: 0; padding: 0; }
1216
- @media (prefers-reduced-motion: reduce) {
1217
- *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
1218
- }
1219
1216
 
1220
1217
  /* ===========================================================================
1221
1218
  * Reduce Motion: SETIAP transisi dinetralkan, bukan daftar yang dipelihara tangan.
@@ -1495,7 +1492,7 @@ a { color: var(--color-primary-strong); }
1495
1492
  @media (prefers-reduced-motion:no-preference){.fdy-autocomplete__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
1496
1493
 
1497
1494
  /* Freeday. Avatar */
1498
- .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
1495
+ .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;-webkit-user-select:none;user-select:none;}
1499
1496
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
1500
1497
  /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
1501
1498
  `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
@@ -1651,7 +1648,7 @@ a { color: var(--color-primary-strong); }
1651
1648
  .fdy-breadcrumb__link:hover{color:var(--color-primary-strong);text-decoration:underline;}
1652
1649
  .fdy-breadcrumb__link:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(var(--color-primary-rgb),.26);}
1653
1650
  .fdy-breadcrumb__item[aria-current="page"]{color:var(--color-text);font-weight:var(--weight-medium);}
1654
- .fdy-breadcrumb__sep{color:var(--color-text-subtle);user-select:none;}
1651
+ .fdy-breadcrumb__sep{color:var(--color-text-subtle);-webkit-user-select:none;user-select:none;}
1655
1652
 
1656
1653
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
1657
1654
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
@@ -1957,7 +1954,13 @@ a { color: var(--color-primary-strong); }
1957
1954
  * size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
1958
1955
  * takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
1959
1956
  * its own content, so it never sized to the table to begin with. */
1960
- .fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
1957
+ .fdy-cfl__results{overflow:auto;max-height:min(24rem,50vh);}
1958
+ /* Fenced the way composition.css:62 fences its own, so the declaration states the condition it
1959
+ needs instead of relying on a browser to drop it. Safari 15.4 to 15.8 is inside the floor the
1960
+ README states and has no container queries at all. */
1961
+ @supports (container-type:inline-size){
1962
+ .fdy-cfl__results{container:fdy-cfl / inline-size;}
1963
+ }
1961
1964
  .fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
1962
1965
 
1963
1966
  .fdy-cfl__row{cursor:pointer;}
@@ -1999,30 +2002,71 @@ a { color: var(--color-primary-strong); }
1999
2002
  *
2000
2003
  * Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
2001
2004
  * row that costs a third of the width buys nothing when each row is labelled by its own title. */
2002
- @container fdy-cfl (max-width: 30rem){
2003
- .fdy-cfl__results .fdy-table,
2004
- .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
2005
- .fdy-cfl__results .fdy-table thead{display:none;}
2006
- .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
2007
- .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
2008
- .fdy-cfl__row > td{display:block;padding:0;border:0;}
2009
- /* The title takes the whole first line, which is what pushes every remaining cell onto the second
2010
- one — no selector has to know how many columns there are, or which of them is last. */
2011
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
2012
- .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
2013
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
2014
- /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
2015
- .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
2016
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
2017
- .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
2018
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
2019
- .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
2020
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
2021
- .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
2022
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
2023
- .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
2024
- /* The checkbox keeps its own line position beside the title rather than above it. */
2025
- .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
2005
+ @supports (container-type:inline-size){
2006
+ @container fdy-cfl (max-width: 30rem){
2007
+ .fdy-cfl__results .fdy-table,
2008
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
2009
+ .fdy-cfl__results .fdy-table thead{display:none;}
2010
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
2011
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
2012
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
2013
+ /* The title takes the whole first line, which is what pushes every remaining cell onto the second
2014
+ one — no selector has to know how many columns there are, or which of them is last. */
2015
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
2016
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
2017
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
2018
+ /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
2019
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
2020
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
2021
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
2022
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
2023
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
2024
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
2025
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
2026
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
2027
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
2028
+ /* The checkbox keeps its own line position beside the title rather than above it. */
2029
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
2030
+ }
2031
+ }
2032
+
2033
+ /* The same block for a browser that has no container queries, which is every Safari from 15.4 to
2034
+ * 15.8 and inside the floor the README states. There the @supports above is false, the stacked
2035
+ * layout never applied, and a 372px dialog kept a three-column master-data table that scrolls on
2036
+ * both axes.
2037
+ *
2038
+ * The condition is a VIEWPORT width, the only width this path can read. 32rem is derived from the
2039
+ * dialog rather than picked: the dialog is min(46rem, 100vw - var(--space-8)) with --space-8 at
2040
+ * 2rem, and the results box takes that width, so it sits at or under 30rem exactly when the
2041
+ * viewport sits at or under 32rem. What a media query cannot answer is the case the container
2042
+ * query exists for, a narrow dialog inside a wide viewport, which is a docked panel on a desktop;
2043
+ * on those browsers that one case keeps the scrolling table it has always had.
2044
+ *
2045
+ * The body is a copy of the body above, because CSS has no way to share one declaration block
2046
+ * between two different conditions. test/baseline.test.mjs compares the two, so an edit that
2047
+ * reaches one of them and misses the other fails there. */
2048
+ @supports not (container-type:inline-size){
2049
+ @media (max-width:32rem){
2050
+ .fdy-cfl__results .fdy-table,
2051
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
2052
+ .fdy-cfl__results .fdy-table thead{display:none;}
2053
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
2054
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
2055
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
2056
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
2057
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
2058
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
2059
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
2060
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
2061
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
2062
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
2063
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
2064
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
2065
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
2066
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
2067
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
2068
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
2069
+ }
2026
2070
  }
2027
2071
 
2028
2072
  /* Freeday. Charts (zero-dependency CSS/SVG, rendered by freeday-chart.js).
package/dist/freeday.css CHANGED
@@ -36,9 +36,6 @@ a { color: var(--color-primary-strong); }
36
36
  * framework with its preflight OFF (the recommended pairing), a semantic <ul>/<ol> keeps native
37
37
  * bullets + a 40px indent; add this class (or a Freeday list component) to normalise it. */
38
38
  .fdy-list-reset { list-style: none; margin: 0; padding: 0; }
39
- @media (prefers-reduced-motion: reduce) {
40
- *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
41
- }
42
39
 
43
40
  /* ===========================================================================
44
41
  * Reduce Motion: SETIAP transisi dinetralkan, bukan daftar yang dipelihara tangan.
@@ -318,7 +315,7 @@ a { color: var(--color-primary-strong); }
318
315
  @media (prefers-reduced-motion:no-preference){.fdy-autocomplete__listbox:not([hidden]){animation:fdy-combo-in var(--dur-fast) var(--ease-standard);}}
319
316
 
320
317
  /* Freeday. Avatar */
321
- .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
318
+ .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;-webkit-user-select:none;user-select:none;}
322
319
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
323
320
  /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
324
321
  `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
@@ -474,7 +471,7 @@ a { color: var(--color-primary-strong); }
474
471
  .fdy-breadcrumb__link:hover{color:var(--color-primary-strong);text-decoration:underline;}
475
472
  .fdy-breadcrumb__link:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(var(--color-primary-rgb),.26);}
476
473
  .fdy-breadcrumb__item[aria-current="page"]{color:var(--color-text);font-weight:var(--weight-medium);}
477
- .fdy-breadcrumb__sep{color:var(--color-text-subtle);user-select:none;}
474
+ .fdy-breadcrumb__sep{color:var(--color-text-subtle);-webkit-user-select:none;user-select:none;}
478
475
 
479
476
  /* Freeday. Responsive display utilities (pairs with freeday-breakpoint.js).
480
477
  * Breakpoint scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920.
@@ -780,7 +777,13 @@ a { color: var(--color-primary-strong); }
780
777
  * size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
781
778
  * takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
782
779
  * its own content, so it never sized to the table to begin with. */
783
- .fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
780
+ .fdy-cfl__results{overflow:auto;max-height:min(24rem,50vh);}
781
+ /* Fenced the way composition.css:62 fences its own, so the declaration states the condition it
782
+ needs instead of relying on a browser to drop it. Safari 15.4 to 15.8 is inside the floor the
783
+ README states and has no container queries at all. */
784
+ @supports (container-type:inline-size){
785
+ .fdy-cfl__results{container:fdy-cfl / inline-size;}
786
+ }
784
787
  .fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
785
788
 
786
789
  .fdy-cfl__row{cursor:pointer;}
@@ -822,30 +825,71 @@ a { color: var(--color-primary-strong); }
822
825
  *
823
826
  * Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
824
827
  * row that costs a third of the width buys nothing when each row is labelled by its own title. */
825
- @container fdy-cfl (max-width: 30rem){
826
- .fdy-cfl__results .fdy-table,
827
- .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
828
- .fdy-cfl__results .fdy-table thead{display:none;}
829
- .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
830
- .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
831
- .fdy-cfl__row > td{display:block;padding:0;border:0;}
832
- /* The title takes the whole first line, which is what pushes every remaining cell onto the second
833
- one — no selector has to know how many columns there are, or which of them is last. */
834
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
835
- .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
836
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
837
- /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
838
- .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
839
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
840
- .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
841
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
842
- .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
843
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
844
- .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
845
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
846
- .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
847
- /* The checkbox keeps its own line position beside the title rather than above it. */
848
- .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
828
+ @supports (container-type:inline-size){
829
+ @container fdy-cfl (max-width: 30rem){
830
+ .fdy-cfl__results .fdy-table,
831
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
832
+ .fdy-cfl__results .fdy-table thead{display:none;}
833
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
834
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
835
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
836
+ /* The title takes the whole first line, which is what pushes every remaining cell onto the second
837
+ one — no selector has to know how many columns there are, or which of them is last. */
838
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
839
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
840
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
841
+ /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
842
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
843
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
844
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
845
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
846
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
847
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
848
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
849
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
850
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
851
+ /* The checkbox keeps its own line position beside the title rather than above it. */
852
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
853
+ }
854
+ }
855
+
856
+ /* The same block for a browser that has no container queries, which is every Safari from 15.4 to
857
+ * 15.8 and inside the floor the README states. There the @supports above is false, the stacked
858
+ * layout never applied, and a 372px dialog kept a three-column master-data table that scrolls on
859
+ * both axes.
860
+ *
861
+ * The condition is a VIEWPORT width, the only width this path can read. 32rem is derived from the
862
+ * dialog rather than picked: the dialog is min(46rem, 100vw - var(--space-8)) with --space-8 at
863
+ * 2rem, and the results box takes that width, so it sits at or under 30rem exactly when the
864
+ * viewport sits at or under 32rem. What a media query cannot answer is the case the container
865
+ * query exists for, a narrow dialog inside a wide viewport, which is a docked panel on a desktop;
866
+ * on those browsers that one case keeps the scrolling table it has always had.
867
+ *
868
+ * The body is a copy of the body above, because CSS has no way to share one declaration block
869
+ * between two different conditions. test/baseline.test.mjs compares the two, so an edit that
870
+ * reaches one of them and misses the other fails there. */
871
+ @supports not (container-type:inline-size){
872
+ @media (max-width:32rem){
873
+ .fdy-cfl__results .fdy-table,
874
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
875
+ .fdy-cfl__results .fdy-table thead{display:none;}
876
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
877
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
878
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
879
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
880
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
881
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
882
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
883
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
884
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
885
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
886
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
887
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
888
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
889
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
890
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
891
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
892
+ }
849
893
  }
850
894
 
851
895
  /* Freeday. Charts (zero-dependency CSS/SVG, rendered by freeday-chart.js).
package/dist/freeday.js CHANGED
@@ -4945,6 +4945,14 @@
4945
4945
  trigger.setAttribute('aria-haspopup', 'listbox');
4946
4946
  trigger.setAttribute('aria-expanded', 'false');
4947
4947
  trigger.setAttribute('aria-label', label);
4948
+ /* The trigger is a <button>, which is NOT a labelable element, so a form's own `<label for>`
4949
+ can never activate it -- but an id is still what `aria-describedby` targets and what a host
4950
+ needs to point at this control. Both were unreadable from the seed until now, so a Blazor
4951
+ host could not name its own error text. Mirrors the datepicker's seed. */
4952
+ var seedId = wrap.getAttribute('data-id');
4953
+ if (seedId) trigger.id = seedId;
4954
+ var describedby = wrap.getAttribute('data-describedby');
4955
+ if (describedby) trigger.setAttribute('aria-describedby', describedby);
4948
4956
  var valueSpan = document.createElement('span');
4949
4957
  valueSpan.className = 'fdy-timepicker__value';
4950
4958
  trigger.appendChild(valueSpan);
@@ -4982,6 +4990,30 @@
4982
4990
  wrap.appendChild(panel);
4983
4991
 
4984
4992
  var active = -1;
4993
+ /* A host that renders its seed ONCE and then never re-renders it (Blazor: ShouldRender is
4994
+ false after hydration) still has to be able to disable, lock or invalidate the field
4995
+ afterwards. Without this the parameter was there and nothing read it. Mirrors
4996
+ FreedayDatepicker.setState so the two controls of one family answer the same call. */
4997
+ var stateDisabled = wrap.hasAttribute('data-disabled');
4998
+ var stateReadonly = wrap.hasAttribute('data-readonly');
4999
+ var stateInvalid = wrap.hasAttribute('data-invalid');
5000
+
5001
+ function applyState() {
5002
+ trigger.disabled = stateDisabled;
5003
+ if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
5004
+ else trigger.removeAttribute('aria-readonly');
5005
+ if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
5006
+ else trigger.removeAttribute('aria-invalid');
5007
+ wrap.classList.toggle('fdy-timepicker--error', stateInvalid);
5008
+ if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
5009
+ }
5010
+ function setState(next) {
5011
+ if (!next) return;
5012
+ if (next.disabled != null) stateDisabled = !!next.disabled;
5013
+ if (next.readonly != null) stateReadonly = !!next.readonly;
5014
+ if (next.invalid != null) stateInvalid = !!next.invalid;
5015
+ applyState();
5016
+ }
4985
5017
 
4986
5018
  function updateDisplay() {
4987
5019
  if (value) {
@@ -5010,6 +5042,7 @@
5010
5042
  var _pop = null;
5011
5043
  function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
5012
5044
  function open() {
5045
+ if (stateDisabled || stateReadonly) return;
5013
5046
  if (!panel.hidden) return;
5014
5047
  var p = popCtl(); if (p) p.show(); else panel.hidden = false;
5015
5048
  trigger.setAttribute('aria-expanded', 'true');
@@ -5060,10 +5093,12 @@
5060
5093
  });
5061
5094
 
5062
5095
  updateDisplay();
5096
+ applyState();
5063
5097
 
5064
5098
  var api = {
5065
5099
  wrap: wrap,
5066
5100
  getValue: function () { return value; },
5101
+ setState: setState,
5067
5102
  clear: function () { value = ''; for (var i = 0; i < opts.length; i++) opts[i].setAttribute('aria-selected', 'false'); updateDisplay(); }
5068
5103
  };
5069
5104
  wrap._fdyTp = api;
@@ -5083,7 +5118,15 @@
5083
5118
  initAll();
5084
5119
  }
5085
5120
 
5086
- window.FreedayTimepicker = { init: initTime, initAll: initAll };
5121
+ window.FreedayTimepicker = {
5122
+ init: initTime,
5123
+ initAll: initAll,
5124
+ /* See api.setState above: the one call a render-once host needs. */
5125
+ setState: function (root, state) {
5126
+ var api = root && root._fdyTp ? root._fdyTp : null;
5127
+ if (api && api.setState) api.setState(state);
5128
+ }
5129
+ };
5087
5130
  })();
5088
5131
 
5089
5132
  /* Freeday, toast API (optional, zero-dependency).
@@ -33,9 +33,9 @@ and the code looks correct and fails later.**
33
33
  | Blazor (net8.0) | `@using Freeday.Blazor` (RCL) | `@bind-Value` |
34
34
  | Static HTML, Svelte, server-rendered templates… | no wrapper: raw markup + the enhancer script | `fdy-*` DOM events |
35
35
 
36
- The twelve: **FdyCombo · FdyDatepicker · FdyMonthpicker · FdyDateRange · FdyAutocomplete ·
37
- FdyCascade · FdyCfl · FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
38
- Vue/React/Blazor, **never hand-write the raw markup + enhancer for these twelve.** The raw path *appears* to work: the enhancer auto-initialises
36
+ The thirteen: **FdyCombo · FdyDatepicker · FdyMonthpicker · FdyDateRange · FdyTimepicker ·
37
+ FdyAutocomplete · FdyCascade · FdyCfl · FdyChart · FdyTable · FdyModal · FdyDrawer · FdyAppShell**. In
38
+ Vue/React/Blazor, **never hand-write the raw markup + enhancer for these thirteen.** The raw path *appears* to work: the enhancer auto-initialises
39
39
  once on `DOMContentLoaded` and the first render is correct. Then it fails quietly: DOM your framework
40
40
  renders later is never hydrated, and the widget's state lives in the DOM instead of in your
41
41
  framework's state.
@@ -21,8 +21,8 @@ Freeday = **CSS** (semantic tokens + `fdy-*` classes) + **zero-dependency JS enh
21
21
  the enhancer owns the widget's DOM. You **listen for `fdy-*` events** (all bubbling
22
22
  `CustomEvent`s, data in `event.detail`) → store them in your framework state. Event/API contract
23
23
  table: [`integrations.md` §Event & API contract](integrations.md).
24
- **On Vue, React or Blazor this is not the path to take for twelve of the components.** `FdyCombo`,
25
- `FdyDatepicker`, `FdyMonthpicker`, `FdyDateRange`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
24
+ **On Vue, React or Blazor this is not the path to take for thirteen of the components.** `FdyCombo`,
25
+ `FdyDatepicker`, `FdyMonthpicker`, `FdyDateRange`, `FdyTimepicker`, `FdyAutocomplete`, `FdyCascade`, `FdyCfl`,
26
26
  `FdyChart`, `FdyTable`, `FdyModal`, `FdyDrawer`, `FdyAppShell` ship typed wrappers that own the state properly (Vue and
27
27
  React re-implement the interaction natively; Blazor wraps the enhancer over interop). Use them;
28
28
  the raw path is for the components without a wrapper, and for stacks without an adapter.
@@ -156,6 +156,13 @@ cp -r node_modules/@cahyo-dimas/freeday/dist ./assets/freeday # copy into your
156
156
  ```
157
157
  > Two-file alternative: `freeday.tokens.css` (tokens) + `freeday.css` (components).
158
158
 
159
+ > **The viewport meta is required, not decoration.** Without it a mobile engine lays the page out at
160
+ > a default width near 980px, so `(max-width: 720px)` never matches and the kit's entire responsive
161
+ > layer is absent: the app shell stands in its desktop form on a phone, sidebar visible and bottom
162
+ > nav gone. That is not a hypothetical. It is what a fixture without the tag did on the first run of
163
+ > the WebKit suite, reporting `window.innerWidth` 981 on an iPhone 7 descriptor, and it is the same
164
+ > shape as the report that started that suite.
165
+
159
166
  ### 3. Load the enhancers before `</body>`
160
167
  ```html
161
168
  <script src="assets/freeday/freeday.js" defer></script>
@@ -191,7 +198,7 @@ live docs also have a copy button per component.
191
198
  ```bash
192
199
  npm i @cahyo-dimas/freeday
193
200
  ```
194
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.9.4"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.11.0"` (public npm package). `dist/` is
195
202
  committed and published → no build step; `npm ci` runs without auth.
196
203
 
197
204
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -444,9 +451,9 @@ kit's CSS/enhancers), then bind:
444
451
  <FdyChart Type="donut" Values="_byCity" Labels="_cityLabels" AriaLabel="Revenue by city" />
445
452
  <FdyDrawer @bind-Open="_drawerOpen" Title="Detail" Side="right">…</FdyDrawer>
446
453
  ```
447
- Twelve components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
454
+ Thirteen components at parity with the Vue/React adapters: **`FdyModal`** · **`FdyDrawer`** (`@bind-Open`,
448
455
  `Title`, `Size`/`Side`, `Dismissible`) · **`FdyCombo<TValue>`** · **`FdyDatepicker`** ·
449
- **`FdyMonthpicker`** (`@bind-Value` as `yyyy-MM`) · **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
456
+ **`FdyMonthpicker`** (`@bind-Value` as `yyyy-MM`) · **`FdyTimepicker`** (`@bind-Value` as `HH:MM`) · **`FdyAutocomplete`** · **`FdyCascade`** · **`FdyDateRange`** (`@bind-From`/`@bind-To`) ·
450
457
  **`FdyCfl<TRow>`** (async `LoadPage`) · **`FdyChart`** · **`FdyTable<TRow>`** (client sort/filter/page,
451
458
  or controlled `Sort`/`Filters`/`Page` for a server-paged table; `RowActivatable`, `RowDetail`) ·
452
459
  **`FdyAppShell`** (`@bind-NavOpen`). Each
@@ -154,7 +154,7 @@ function Panel(): JSX.Element {
154
154
  ```
155
155
  > **Controlled alternative (parity with Vue's `v-model`):** `@cahyo-dimas/freeday/react` also exports
156
156
  > the typed components `FdyCombo` / `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` /
157
- > `FdyAutocomplete` / `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
157
+ > `FdyTimepicker` / `FdyAutocomplete` / `FdyCascade` / `FdyCfl` / `FdyChart` / `FdyTable` / `FdyModal` / `FdyDrawer` /
158
158
  > `FdyAppShell`, with plain
159
159
  > `value`/`onChange` (props in, events out), no `data-fdy-*` + manual event listener:
160
160
  > ```tsx
@@ -200,7 +200,7 @@ document.addEventListener('fdy-form-invalid', (e) =>
200
200
 
201
201
  > **Native components (parity with Vue's `v-model` / React's `value`/`onChange`):** the
202
202
  > **`Freeday.Blazor`** RCL (net8.0, `adapters/blazor/`) ships the typed components `FdyCombo<TValue>` /
203
- > `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` / `FdyAutocomplete` / `FdyCascade` /
203
+ > `FdyDatepicker` / `FdyMonthpicker` / `FdyDateRange` / `FdyTimepicker` / `FdyAutocomplete` / `FdyCascade` /
204
204
  > `FdyCfl<TRow>` / `FdyChart` / `FdyTable<TRow>` / `FdyModal` / `FdyDrawer` / `FdyAppShell` with `@bind`, no `@ref` / manual `initAll` / `[JSInvokable]`:
205
205
  > ```razor
206
206
  > @using Freeday.Blazor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "3.9.4",
3
+ "version": "3.11.0",
4
4
  "description": "Freeday: token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -96,14 +96,16 @@
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/harness-contract.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/harness-contract.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",
103
103
  "test:blazor": "dotnet test test/blazor -v quiet --nologo",
104
+ "test:webkit": "playwright test",
104
105
  "test:blazor-server": "node --test browser/blazor-server.mjs"
105
106
  },
106
107
  "devDependencies": {
108
+ "@playwright/test": "^1.64.1",
107
109
  "@types/react": "^19.2.17",
108
110
  "esbuild": "^0.28.1",
109
111
  "react": "^19.2.8",
package/src/base.css CHANGED
@@ -35,9 +35,6 @@ a { color: var(--color-primary-strong); }
35
35
  * framework with its preflight OFF (the recommended pairing), a semantic <ul>/<ol> keeps native
36
36
  * bullets + a 40px indent; add this class (or a Freeday list component) to normalise it. */
37
37
  .fdy-list-reset { list-style: none; margin: 0; padding: 0; }
38
- @media (prefers-reduced-motion: reduce) {
39
- *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
40
- }
41
38
 
42
39
  /* ===========================================================================
43
40
  * Reduce Motion: SETIAP transisi dinetralkan, bukan daftar yang dipelihara tangan.
@@ -1,5 +1,5 @@
1
1
  /* Freeday. Avatar */
2
- .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;user-select:none;}
2
+ .fdy-avatar{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.5rem;height:2.5rem;border-radius:var(--radius-full);background:var(--color-primary-soft);color:var(--color-primary-strong);font-family:var(--font-body);font-weight:var(--weight-semibold);font-size:var(--text-sm);line-height:1;overflow:hidden;-webkit-user-select:none;user-select:none;}
3
3
  .fdy-avatar img{width:100%;height:100%;object-fit:cover;}
4
4
  /* --xs exists for an avatar INSIDE a control: `.fdy-btn--sm` is exactly 2rem tall, which is also
5
5
  `--sm`'s size, so a small avatar in a small button fills it edge to edge and the ghost border
@@ -6,4 +6,4 @@
6
6
  .fdy-breadcrumb__link:hover{color:var(--color-primary-strong);text-decoration:underline;}
7
7
  .fdy-breadcrumb__link:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(var(--color-primary-rgb),.26);}
8
8
  .fdy-breadcrumb__item[aria-current="page"]{color:var(--color-text);font-weight:var(--weight-medium);}
9
- .fdy-breadcrumb__sep{color:var(--color-text-subtle);user-select:none;}
9
+ .fdy-breadcrumb__sep{color:var(--color-text-subtle);-webkit-user-select:none;user-select:none;}
@@ -23,7 +23,13 @@
23
23
  * size, which is what made `.fdy-stats--inline` collapse to three zero tracks in 3.1.0. This box
24
24
  * takes its width from the dialog (`min(46rem, 100vw - …)`, an explicit width) and already scrolls
25
25
  * its own content, so it never sized to the table to begin with. */
26
- .fdy-cfl__results{container:fdy-cfl / inline-size;overflow:auto;max-height:min(24rem,50vh);}
26
+ .fdy-cfl__results{overflow:auto;max-height:min(24rem,50vh);}
27
+ /* Fenced the way composition.css:62 fences its own, so the declaration states the condition it
28
+ needs instead of relying on a browser to drop it. Safari 15.4 to 15.8 is inside the floor the
29
+ README states and has no container queries at all. */
30
+ @supports (container-type:inline-size){
31
+ .fdy-cfl__results{container:fdy-cfl / inline-size;}
32
+ }
27
33
  .fdy-cfl__results .fdy-table th{position:sticky;top:0;z-index:1;}
28
34
 
29
35
  .fdy-cfl__row{cursor:pointer;}
@@ -65,28 +71,69 @@
65
71
  *
66
72
  * Column headers ARE a real loss here, and a deliberate one: `<thead>` is hidden, because a header
67
73
  * row that costs a third of the width buys nothing when each row is labelled by its own title. */
68
- @container fdy-cfl (max-width: 30rem){
69
- .fdy-cfl__results .fdy-table,
70
- .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
71
- .fdy-cfl__results .fdy-table thead{display:none;}
72
- .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
73
- .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
74
- .fdy-cfl__row > td{display:block;padding:0;border:0;}
75
- /* The title takes the whole first line, which is what pushes every remaining cell onto the second
76
- one — no selector has to know how many columns there are, or which of them is last. */
77
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
78
- .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
79
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
80
- /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
81
- .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
82
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
83
- .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
84
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
85
- .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
86
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
87
- .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
88
- .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
89
- .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
90
- /* The checkbox keeps its own line position beside the title rather than above it. */
91
- .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
74
+ @supports (container-type:inline-size){
75
+ @container fdy-cfl (max-width: 30rem){
76
+ .fdy-cfl__results .fdy-table,
77
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
78
+ .fdy-cfl__results .fdy-table thead{display:none;}
79
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
80
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
81
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
82
+ /* The title takes the whole first line, which is what pushes every remaining cell onto the second
83
+ one — no selector has to know how many columns there are, or which of them is last. */
84
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
85
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
86
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
87
+ /* Multi-select: the checkbox shares line one with the title, so the title claims the rest of it. */
88
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
89
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
90
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
91
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
92
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
93
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
94
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
95
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
96
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
97
+ /* The checkbox keeps its own line position beside the title rather than above it. */
98
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
99
+ }
100
+ }
101
+
102
+ /* The same block for a browser that has no container queries, which is every Safari from 15.4 to
103
+ * 15.8 and inside the floor the README states. There the @supports above is false, the stacked
104
+ * layout never applied, and a 372px dialog kept a three-column master-data table that scrolls on
105
+ * both axes.
106
+ *
107
+ * The condition is a VIEWPORT width, the only width this path can read. 32rem is derived from the
108
+ * dialog rather than picked: the dialog is min(46rem, 100vw - var(--space-8)) with --space-8 at
109
+ * 2rem, and the results box takes that width, so it sits at or under 30rem exactly when the
110
+ * viewport sits at or under 32rem. What a media query cannot answer is the case the container
111
+ * query exists for, a narrow dialog inside a wide viewport, which is a docked panel on a desktop;
112
+ * on those browsers that one case keeps the scrolling table it has always had.
113
+ *
114
+ * The body is a copy of the body above, because CSS has no way to share one declaration block
115
+ * between two different conditions. test/baseline.test.mjs compares the two, so an edit that
116
+ * reaches one of them and misses the other fails there. */
117
+ @supports not (container-type:inline-size){
118
+ @media (max-width:32rem){
119
+ .fdy-cfl__results .fdy-table,
120
+ .fdy-cfl__results .fdy-table tbody{display:block;width:100%;}
121
+ .fdy-cfl__results .fdy-table thead{display:none;}
122
+ .fdy-cfl__row{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border-muted);}
123
+ .fdy-cfl__results .fdy-table tbody tr:last-child{border-bottom:0;}
124
+ .fdy-cfl__row > td{display:block;padding:0;border:0;}
125
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check),
126
+ .fdy-cfl__row > .fdy-cfl__check + td{font-weight:var(--weight-semibold);color:var(--color-text);}
127
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check){flex:0 0 100%;}
128
+ .fdy-cfl__row > .fdy-cfl__check + td{flex:1 0 calc(100% - var(--space-6));}
129
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td,
130
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td{font-size:var(--text-xs);color:var(--color-text-muted);}
131
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td:empty,
132
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td:empty{display:none;}
133
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) ~ td::before,
134
+ .fdy-cfl__row > .fdy-cfl__check + td ~ td::before{content:"·";margin-right:var(--space-2);color:var(--color-text-subtle);}
135
+ .fdy-cfl__row > td:first-child:not(.fdy-cfl__check) + td::before,
136
+ .fdy-cfl__row > .fdy-cfl__check + td + td::before{content:none;}
137
+ .fdy-cfl__row > .fdy-cfl__check{flex:none;width:auto;align-self:center;}
138
+ }
92
139
  }