@cahyo-dimas/freeday 1.36.0 → 1.38.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
@@ -3,6 +3,54 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.38.0] — 2026-08-19
7
+ One note from the back-office app (IDU_EMATE_APPL_WEB, #015).
8
+ ### Fixed
9
+ - **`FdyCfl`'s trigger said `"Buka pencarian"`** (Vue · React) — the accessible name of the button
10
+ that opens the picker, and so the first thing a screen-reader user meets on every
11
+ choose-from-list. #009 replaced twelve strings around it and missed this one. It takes an
12
+ `openLabel` prop defaulting to `'Open search'` now. Blazor was not Indonesian but was using
13
+ `SearchPlaceholder` as that button's `aria-label` — a placeholder doing a label's job — and now
14
+ takes the same `OpenLabel`, so all four stacks name the control identically.
15
+ - **`FdyCascade`'s up-one-level button said `"Kembali satu tingkat"`** (Vue · React) — new
16
+ `backLabel`, default `'Back one level'`.
17
+ ### Changed
18
+ - **The language guard now lists derived forms, not just roots.** `\bcari\b` cannot match
19
+ *pen·cari·an*, and `kembali` was never listed — Indonesian derives by affix, so a whole-word list
20
+ misses most of the UI vocabulary. Widening it is what surfaced the `FdyCascade` string nobody had
21
+ reported.
22
+
23
+ ### Fixed — the test suite
24
+ - **`rules()` read only the last line of a selector**, so a rule written across several lines was
25
+ reported under a fragment of its own name and every guard in `test/css.test.mjs` was blind to it
26
+ (#014). A guard that matches nothing PASSES, which is the worst way for a test to fail — it
27
+ surfaced only because the first draft of the filter-bar rule happened to be wrapped. The helper
28
+ now strips comments from the whole source first (a comment quoting CSS carries braces, and those
29
+ would split a rule in half) and reads the selector whole. Asserted directly rather than left to
30
+ the CSS staying one-rule-per-line: that is a convention, and this is the thing that would quietly
31
+ stop enforcing it. Mutation-tested — restoring the last-line read fails the new test by name.
32
+
33
+ ## [1.37.0] — 2026-08-19
34
+ One note from the back-office app (IDU_EMATE_APPL_WEB, #014).
35
+ ### Added
36
+ - **`.fdy-table__freeze` + `--fdy-freeze-left`** — freeze **any number** of columns, not just the
37
+ first. 1.35.0 froze `tbody th` at a hard-coded `left:0`, which suits a rate matrix's single row
38
+ header and cannot express a wide table with two identity columns: they all landed on 0 and
39
+ stacked. Mark every cell of a frozen column and give that column its offset; the offsets belong
40
+ to the caller because they are sums of *rendered* widths, which CSS cannot compute.
41
+ `.fdy-table__freeze--edge` marks where the frozen block ends.
42
+ - **A bare control in a `.fdy-filterbar` gets `--control-h`.** The bar aligns `flex-end` so
43
+ labelled fields line up on their inputs; a `.fdy-check` / `.fdy-switch` / `.fdy-radio` / lone
44
+ `.fdy-btn` has no label row, so flex-end left it sitting below the input's centre line. Now its
45
+ box ends where an input's box ends and the control centres on that line.
46
+ ### Changed
47
+ - **BREAKING (from 1.35.0): the header freeze moved to `.fdy-table--sticky-head`.** Freezing a row
48
+ needs a vertical scrollport; a table that only freezes columns scrolls with the **page**, where
49
+ `top:0` sticks the header under the viewport edge — over the consuming app's own top bar.
50
+ `.fdy-table--sticky` is now the enabler (separate borders) and each axis is asked for
51
+ explicitly. `tbody th` no longer freezes implicitly; use `.fdy-table__freeze`. 1.35.0 and 1.36.0
52
+ are unpublished, so no released version carried the old contract.
53
+
6
54
  ## [1.36.0] — 2026-08-19
7
55
  One note from the back-office app (IDU_EMATE_APPL_WEB, #013).
8
56
  ### Added
package/COMPONENTS.md CHANGED
@@ -639,7 +639,11 @@ Responsive two-column document header. Children are `.fdy-field`; `.fdy-field--f
639
639
  ## Filter bar — `.fdy-filterbar`
640
640
  A consistent filter row of `.fdy-field`s with a width rhythm (`--w-sm` · default · `--w-lg` ·
641
641
  `--w-xl` · `--w-2xl`), one `--w-grow` field (usually search) absorbing the slack, and
642
- `.fdy-filterbar__actions` pinned at the end. `--actions-inline` keeps the actions on the control
642
+ `.fdy-filterbar__actions` pinned at the end. A control with **no label of its own** (a
643
+ `.fdy-check`, `.fdy-switch`, `.fdy-radio`, or a lone `.fdy-btn`) is given `--control-h` so it
644
+ centres on the input line instead of hanging below it — the bar aligns `flex-end` to line up
645
+ labelled fields, which would otherwise drop a 20px checkbox below a 32px input's centre.
646
+ `--actions-inline` keeps the actions on the control
643
647
  line. Zero JS. Composes with `.fdy-table-toolbar` on a shared baseline.
644
648
 
645
649
  ## Slider — `.fdy-slider`
@@ -695,20 +699,36 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
695
699
  ```
696
700
 
697
701
  **Frozen axes — `.fdy-table--sticky`.** For a grid read against two axes at once (a rate
698
- matrix, a timetable). Put `.fdy-table-scroll--frozen` on the wrapper it scrolls both ways
699
- and is the scrollport the frozen cells stick to and size it with `--fdy-table-frozen-h`
700
- (default `30rem`). `<thead>` freezes at the top, `<th scope="row">` freezes at the left, and
701
- the first header cell is the corner. Do not reach for `position:sticky` directly: the base
702
- table collapses its borders, and a collapsed border belongs to the table rather than the
703
- cell, so it scrolls out from under whatever you froze.
702
+ matrix, a timetable) or a wide one whose identity columns must stay visible. `.fdy-table--sticky`
703
+ is the enabler: it separates the borders, because a collapsed border belongs to the table rather
704
+ than the cell and scrolls out from under whatever you froze. Then ask for the axes you have:
705
+
706
+ - **Header row** add `.fdy-table--sticky-head`, and put `.fdy-table-scroll--frozen` on the
707
+ wrapper (it scrolls both ways and is the scrollport the freeze resolves against; size it with
708
+ `--fdy-table-frozen-h`, default `30rem`). It is a separate modifier because a table that only
709
+ freezes columns scrolls with the **page**, and `top:0` against the page sticks the header under
710
+ the viewport edge — over the app's own top bar.
711
+ - **Columns** — put `.fdy-table__freeze` on **every cell of the column, header included**, and give
712
+ each frozen column its own `--fdy-freeze-left`: `0` for the first, then the summed widths of the
713
+ frozen columns to its left. Mark the last one `.fdy-table__freeze--edge` so the reader can see
714
+ where the frozen block ends. One frozen column needs no variable at all.
715
+
716
+ The offsets are the caller's job because they depend on rendered column widths, which CSS cannot
717
+ sum — measure them once after render (and on resize) and set the variable.
704
718
 
705
719
  ```html
706
720
  <div class="fdy-table-scroll fdy-table-scroll--frozen" style="--fdy-table-frozen-h:26rem">
707
- <table class="fdy-table fdy-table--sticky">
721
+ <table class="fdy-table fdy-table--sticky fdy-table--sticky-head">
708
722
  <caption class="fdy-visually-hidden">Exchange rates, August 2026</caption>
709
- <thead><tr><th scope="col">Date</th><th scope="col" class="fdy-table__num">USD</th></tr></thead>
723
+ <thead><tr>
724
+ <th scope="col" class="fdy-table__freeze fdy-table__freeze--edge">Date</th>
725
+ <th scope="col" class="fdy-table__num">USD</th>
726
+ </tr></thead>
710
727
  <tbody>
711
- <tr><th scope="row">1 Aug</th><td class="fdy-table__num">16,240</td></tr>
728
+ <tr>
729
+ <th scope="row" class="fdy-table__freeze fdy-table__freeze--edge">1 Aug</th>
730
+ <td class="fdy-table__num">16,240</td>
731
+ </tr>
712
732
  </tbody>
713
733
  </table>
714
734
  </div>
@@ -21,7 +21,7 @@
21
21
  </button>
22
22
  }
23
23
  <button class="fdy-input-group__btn" type="button" @onclick="OpenAsync"
24
- disabled="@(Disabled || Readonly)" aria-label="@SearchPlaceholder">
24
+ disabled="@(Disabled || Readonly)" aria-label="@OpenLabel">
25
25
  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
26
26
  stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
27
27
  <circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path>
@@ -32,6 +32,9 @@ public partial class FdyCfl<TRow>
32
32
  [Parameter] public string MoreText { get; set; } = "Load more";
33
33
  [Parameter] public string CloseLabel { get; set; } = "Close";
34
34
 
35
+ /// <summary>aria-label for the button that opens the picker.</summary>
36
+ [Parameter] public string OpenLabel { get; set; } = "Open search";
37
+
35
38
  [Parameter] public bool Disabled { get; set; }
36
39
 
37
40
  /// <summary>Locked/view mode: shows the value but the search dialog can't be opened.</summary>
@@ -23,6 +23,8 @@ export interface FdyCascadeProps {
23
23
  options: ReadonlyArray<CascadeNode>;
24
24
  /** Path separator in the display, default " / " (matches the enhancer). */
25
25
  separator?: string;
26
+ /** aria-label for the button that goes up one level. Default 'Back one level'. */
27
+ backLabel?: string;
26
28
  placeholder?: string;
27
29
  /** Accessible name for the trigger + listbox (the enhancer's data-label). */
28
30
  label?: string;
@@ -191,7 +193,7 @@ export function FdyCascade(props: FdyCascadeProps): JSX.Element {
191
193
  <button
192
194
  type="button"
193
195
  className="fdy-cascade__back"
194
- aria-label="Kembali satu tingkat"
196
+ aria-label={props.backLabel ?? 'Back one level'}
195
197
  hidden={stack.length === 0}
196
198
  onClick={(): void => { ascend(); listRef.current?.focus(); }}
197
199
  >
@@ -43,6 +43,8 @@ export interface FdyCflProps<Row extends Record<string, unknown>> {
43
43
  moreText?: string;
44
44
  /** aria-label for the dialog's close button. Default 'Close'. */
45
45
  closeLabel?: string;
46
+ /** aria-label for the button that opens the picker. Default 'Open search'. */
47
+ openLabel?: string;
46
48
  placeholder?: string;
47
49
  disabled?: boolean;
48
50
  /** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
@@ -300,7 +302,7 @@ export function FdyCfl<Row extends Record<string, unknown>>(props: FdyCflProps<R
300
302
  className="fdy-input-group__btn"
301
303
  aria-haspopup="dialog"
302
304
  aria-labelledby={props.ariaLabelledby}
303
- aria-label={props.ariaLabelledby ? undefined : 'Buka pencarian'}
305
+ aria-label={props.ariaLabelledby ? undefined : (props.openLabel ?? 'Open search')}
304
306
  disabled={isDisabled || isReadonly}
305
307
  onClick={openDialog}
306
308
  >
@@ -36,6 +36,8 @@ const props = defineProps<{
36
36
  options: ReadonlyArray<CascadeNode>;
37
37
  /** Path separator in the display, default " / " (matches the enhancer). */
38
38
  separator?: string;
39
+ /** aria-label for the button that goes up one level. Default 'Back one level'. */
40
+ backLabel?: string;
39
41
  placeholder?: string;
40
42
  /** Accessible name for the trigger + listbox (the enhancer's data-label). */
41
43
  label?: string;
@@ -207,7 +209,7 @@ onBeforeUnmount((): void => {
207
209
  <button
208
210
  type="button"
209
211
  class="fdy-cascade__back"
210
- aria-label="Kembali satu tingkat"
212
+ :aria-label="backLabel ?? 'Back one level'"
211
213
  :hidden="stack.length === 0"
212
214
  @click="ascend(); listEl?.focus()"
213
215
  >
@@ -44,6 +44,8 @@ const props = defineProps<{
44
44
  moreText?: string;
45
45
  /** aria-label for the dialog's close button. Default 'Close'. */
46
46
  closeLabel?: string;
47
+ /** aria-label for the button that opens the picker. Default 'Open search'. */
48
+ openLabel?: string;
47
49
  placeholder?: string;
48
50
  disabled?: boolean;
49
51
  /** Locked/view mode: shows the picked value (focusable, copyable), but the search dialog can't be opened. Unlike `disabled`, it keeps tab order and isn't greyed. */
@@ -306,7 +308,7 @@ onBeforeUnmount((): void => {
306
308
  class="fdy-input-group__btn"
307
309
  aria-haspopup="dialog"
308
310
  :aria-labelledby="ariaLabelledby"
309
- :aria-label="ariaLabelledby ? undefined : 'Buka pencarian'"
311
+ :aria-label="ariaLabelledby ? undefined : (openLabel ?? 'Open search')"
310
312
  :disabled="isDisabled || isReadonly"
311
313
  @click="openDialog"
312
314
  >
@@ -1298,6 +1298,14 @@ a { color: var(--color-primary); }
1298
1298
  * `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
1299
1299
  * not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
1300
1300
  * row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
1301
+ /* A control that carries NO label of its own — a checkbox, a switch, a lone
1302
+ button — in a bar whose `align-items:flex-end` exists to line up LABELLED
1303
+ fields. Flex-end puts such a control's bottom on the inputs' bottom, so a
1304
+ 20px checkbox sits well below the 32px input's centre line and reads as
1305
+ dropped. Giving it the height of a control lands its box exactly where an
1306
+ input's box ends, and its own centring then puts it on the input's centre
1307
+ line. Raised from IDU_EMATE_APPL_WEB (#014). */
1308
+ .fdy-filterbar>.fdy-check,.fdy-filterbar>.fdy-radio,.fdy-filterbar>.fdy-switch,.fdy-filterbar>.fdy-btn{min-height:var(--control-h);}
1301
1309
  .fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
1302
1310
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
1303
1311
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
@@ -1667,15 +1675,37 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1667
1675
  back on the cell that sticks. */
1668
1676
  .fdy-table--sticky{border-collapse:separate;border-spacing:0;}
1669
1677
  .fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
1670
- .fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
1671
- /* The row header freezes sideways over the data cells, so it needs an opaque
1672
- background of its own and it takes the BODY surface, not the header one, so a
1673
- hovered row stays one continuous band across the freeze line. Losing that is
1674
- losing the only way to track a row across a wide grid. */
1675
- .fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
1676
- .fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
1677
- /* The corner cell is frozen on both axes, so it has to outrank both. */
1678
- .fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
1678
+
1679
+ /* Freezing the HEADER ROW is its own modifier because it needs a vertical
1680
+ scrollport (`.fdy-table-scroll--frozen`). A table that only freezes columns
1681
+ scrolls with the PAGE, and `top:0` against the page sticks the header under
1682
+ the viewport edge — over the app's own top bar which is worse than not
1683
+ freezing it. Ask for the axis you actually have. */
1684
+ .fdy-table--sticky-head thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
1685
+
1686
+ /* Freezing a COLUMN: put `.fdy-table__freeze` on every cell of it, header
1687
+ included, and give each frozen column its own `--fdy-freeze-left` — 0 for the
1688
+ first, the summed widths of the frozen columns left of it after that. One
1689
+ frozen column needs no variable at all. Mark the last one `--edge` so the
1690
+ reader can see where the frozen block ends.
1691
+
1692
+ The offsets are the caller's job on purpose: they depend on rendered column
1693
+ widths, which CSS cannot sum, and hard-coding them here would only work for
1694
+ tables whose columns are all the same width. */
1695
+ .fdy-table--sticky .fdy-table__freeze{position:sticky;left:var(--fdy-freeze-left,0);}
1696
+ /* A frozen body cell slides OVER the data, so it must be OPAQUE — otherwise the
1697
+ cells beneath show through it. A `th` already carries the header tint from the
1698
+ base rule, and that tint is usually the whole point: it is what marks a column
1699
+ as identity rather than data. So only a `td` is given a surface here. A table
1700
+ that wants its frozen `th` to read as a body row instead — a rate matrix, where
1701
+ a hovered row should band unbroken across the freeze line — says so itself. */
1702
+ .fdy-table--sticky tbody .fdy-table__freeze{z-index:1;}
1703
+ .fdy-table--sticky tbody td.fdy-table__freeze{background:var(--color-surface);}
1704
+ .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
1705
+ /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
1706
+ it has to outrank both. */
1707
+ .fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
1708
+ .fdy-table--sticky .fdy-table__freeze--edge{border-right:var(--bw) solid var(--color-border);}
1679
1709
 
1680
1710
  /* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
1681
1711
  custom property so a page can size it without out-specifying this rule. */
package/dist/freeday.css CHANGED
@@ -915,6 +915,14 @@ a { color: var(--color-primary); }
915
915
  * `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
916
916
  * not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
917
917
  * row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
918
+ /* A control that carries NO label of its own — a checkbox, a switch, a lone
919
+ button — in a bar whose `align-items:flex-end` exists to line up LABELLED
920
+ fields. Flex-end puts such a control's bottom on the inputs' bottom, so a
921
+ 20px checkbox sits well below the 32px input's centre line and reads as
922
+ dropped. Giving it the height of a control lands its box exactly where an
923
+ input's box ends, and its own centring then puts it on the input's centre
924
+ line. Raised from IDU_EMATE_APPL_WEB (#014). */
925
+ .fdy-filterbar>.fdy-check,.fdy-filterbar>.fdy-radio,.fdy-filterbar>.fdy-switch,.fdy-filterbar>.fdy-btn{min-height:var(--control-h);}
918
926
  .fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
919
927
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
920
928
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
@@ -1284,15 +1292,37 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
1284
1292
  back on the cell that sticks. */
1285
1293
  .fdy-table--sticky{border-collapse:separate;border-spacing:0;}
1286
1294
  .fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
1287
- .fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
1288
- /* The row header freezes sideways over the data cells, so it needs an opaque
1289
- background of its own and it takes the BODY surface, not the header one, so a
1290
- hovered row stays one continuous band across the freeze line. Losing that is
1291
- losing the only way to track a row across a wide grid. */
1292
- .fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
1293
- .fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
1294
- /* The corner cell is frozen on both axes, so it has to outrank both. */
1295
- .fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
1295
+
1296
+ /* Freezing the HEADER ROW is its own modifier because it needs a vertical
1297
+ scrollport (`.fdy-table-scroll--frozen`). A table that only freezes columns
1298
+ scrolls with the PAGE, and `top:0` against the page sticks the header under
1299
+ the viewport edge — over the app's own top bar which is worse than not
1300
+ freezing it. Ask for the axis you actually have. */
1301
+ .fdy-table--sticky-head thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
1302
+
1303
+ /* Freezing a COLUMN: put `.fdy-table__freeze` on every cell of it, header
1304
+ included, and give each frozen column its own `--fdy-freeze-left` — 0 for the
1305
+ first, the summed widths of the frozen columns left of it after that. One
1306
+ frozen column needs no variable at all. Mark the last one `--edge` so the
1307
+ reader can see where the frozen block ends.
1308
+
1309
+ The offsets are the caller's job on purpose: they depend on rendered column
1310
+ widths, which CSS cannot sum, and hard-coding them here would only work for
1311
+ tables whose columns are all the same width. */
1312
+ .fdy-table--sticky .fdy-table__freeze{position:sticky;left:var(--fdy-freeze-left,0);}
1313
+ /* A frozen body cell slides OVER the data, so it must be OPAQUE — otherwise the
1314
+ cells beneath show through it. A `th` already carries the header tint from the
1315
+ base rule, and that tint is usually the whole point: it is what marks a column
1316
+ as identity rather than data. So only a `td` is given a surface here. A table
1317
+ that wants its frozen `th` to read as a body row instead — a rate matrix, where
1318
+ a hovered row should band unbroken across the freeze line — says so itself. */
1319
+ .fdy-table--sticky tbody .fdy-table__freeze{z-index:1;}
1320
+ .fdy-table--sticky tbody td.fdy-table__freeze{background:var(--color-surface);}
1321
+ .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
1322
+ /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
1323
+ it has to outrank both. */
1324
+ .fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
1325
+ .fdy-table--sticky .fdy-table__freeze--edge{border-right:var(--bw) solid var(--color-border);}
1296
1326
 
1297
1327
  /* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
1298
1328
  custom property so a page can size it without out-specifying this rule. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.36.0",
3
+ "version": "1.38.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,6 +28,14 @@
28
28
  * `flex:1 1 16rem` reserves 16rem for line-breaking), NOT its rendered width — so the grow basis,
29
29
  * not the visible width, decides the wrap point. Margins can't keep the actions off a lonely wrapped
30
30
  * row; to avoid stranded actions, tune the --w-grow basis or drop a field so the bar fits one row. */
31
+ /* A control that carries NO label of its own — a checkbox, a switch, a lone
32
+ button — in a bar whose `align-items:flex-end` exists to line up LABELLED
33
+ fields. Flex-end puts such a control's bottom on the inputs' bottom, so a
34
+ 20px checkbox sits well below the 32px input's centre line and reads as
35
+ dropped. Giving it the height of a control lands its box exactly where an
36
+ input's box ends, and its own centring then puts it on the input's centre
37
+ line. Raised from IDU_EMATE_APPL_WEB (#014). */
38
+ .fdy-filterbar>.fdy-check,.fdy-filterbar>.fdy-radio,.fdy-filterbar>.fdy-switch,.fdy-filterbar>.fdy-btn{min-height:var(--control-h);}
31
39
  .fdy-filterbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;}
32
40
  /* Opt-in: sit the actions inline right after the last field instead of at the far edge — so when the
33
41
  * bar wraps they read as attached to the filters rather than shoved to the right of a lonely row. */
@@ -47,15 +47,37 @@
47
47
  back on the cell that sticks. */
48
48
  .fdy-table--sticky{border-collapse:separate;border-spacing:0;}
49
49
  .fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
50
- .fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
51
- /* The row header freezes sideways over the data cells, so it needs an opaque
52
- background of its own and it takes the BODY surface, not the header one, so a
53
- hovered row stays one continuous band across the freeze line. Losing that is
54
- losing the only way to track a row across a wide grid. */
55
- .fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
56
- .fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
57
- /* The corner cell is frozen on both axes, so it has to outrank both. */
58
- .fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
50
+
51
+ /* Freezing the HEADER ROW is its own modifier because it needs a vertical
52
+ scrollport (`.fdy-table-scroll--frozen`). A table that only freezes columns
53
+ scrolls with the PAGE, and `top:0` against the page sticks the header under
54
+ the viewport edge — over the app's own top bar which is worse than not
55
+ freezing it. Ask for the axis you actually have. */
56
+ .fdy-table--sticky-head thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
57
+
58
+ /* Freezing a COLUMN: put `.fdy-table__freeze` on every cell of it, header
59
+ included, and give each frozen column its own `--fdy-freeze-left` — 0 for the
60
+ first, the summed widths of the frozen columns left of it after that. One
61
+ frozen column needs no variable at all. Mark the last one `--edge` so the
62
+ reader can see where the frozen block ends.
63
+
64
+ The offsets are the caller's job on purpose: they depend on rendered column
65
+ widths, which CSS cannot sum, and hard-coding them here would only work for
66
+ tables whose columns are all the same width. */
67
+ .fdy-table--sticky .fdy-table__freeze{position:sticky;left:var(--fdy-freeze-left,0);}
68
+ /* A frozen body cell slides OVER the data, so it must be OPAQUE — otherwise the
69
+ cells beneath show through it. A `th` already carries the header tint from the
70
+ base rule, and that tint is usually the whole point: it is what marks a column
71
+ as identity rather than data. So only a `td` is given a surface here. A table
72
+ that wants its frozen `th` to read as a body row instead — a rate matrix, where
73
+ a hovered row should band unbroken across the freeze line — says so itself. */
74
+ .fdy-table--sticky tbody .fdy-table__freeze{z-index:1;}
75
+ .fdy-table--sticky tbody td.fdy-table__freeze{background:var(--color-surface);}
76
+ .fdy-table--sticky tbody tr:hover td.fdy-table__freeze{background:var(--color-surface-2);}
77
+ /* A frozen header cell is stuck on both axes wherever the head is frozen too, so
78
+ it has to outrank both. */
79
+ .fdy-table--sticky thead .fdy-table__freeze{z-index:3;}
80
+ .fdy-table--sticky .fdy-table__freeze--edge{border-right:var(--bw) solid var(--color-border);}
59
81
 
60
82
  /* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
61
83
  custom property so a page can size it without out-specifying this rule. */