@apliteni/apliteni-ui 0.104.0 → 0.105.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.
@@ -0,0 +1,77 @@
1
+ # Dashboards and reports
2
+
3
+ ## Build a dashboard for one screen
4
+
5
+ <!-- rule: fast-glance -->
6
+
7
+ **Rule:** Answer “is anything wrong, and what changed” in one screen, without scrolling.
8
+
9
+ **Why:** A dashboard is read between other work: below the fold is not read.
10
+
11
+ **Except:** A phone stacks the blocks and scrolls; budget a desktop screen.
12
+
13
+ **Do:** A period control, a row of figures, a short list of exceptions.
14
+
15
+ **Don't:** A second screen of blocks below the figures.
16
+
17
+ ## Put only figures, trends and exceptions on a dashboard
18
+
19
+ <!-- rule: figures-and-exceptions -->
20
+
21
+ **Rule:** Keep a dashboard to figures, trends and the rows that need attention.
22
+
23
+ **Why:** Reading a table is what a report is for.
24
+
25
+ **Do:** The figures with their trends, then the payouts needing a decision.
26
+
27
+ **Don't:** The whole ledger and a paragraph about it.
28
+
29
+ ## Link each reference to its row
30
+
31
+ <!-- rule: link-to-the-report -->
32
+
33
+ **Rule:** Link each reference to its report row and bring that row into view.
34
+
35
+ **Why:** A dashboard stops at what happened; a report opening at its own top has not answered the reference.
36
+
37
+ **Do:** Each payout reference opens its row in the report.
38
+
39
+ **Don't:** A block that lists what is wrong and offers no way in.
40
+
41
+ ## Build a report for the whole answer
42
+
43
+ <!-- rule: report-depth -->
44
+
45
+ **Rule:** Answer “why, and exactly how much”, with the rows behind the figure.
46
+
47
+ **Why:** Analysis needs the rows: a summary cannot say how much.
48
+
49
+ **Do:** The payout ledger, with the fees and net on every row.
50
+
51
+ **Don't:** The same ledger reduced to three figures.
52
+
53
+ ## Give a report its filters and its export
54
+
55
+ <!-- rule: report-offers -->
56
+
57
+ **Rule:** Offer filters and one export on every report.
58
+
59
+ **Why:** Narrowing the question, or taking the rows away, needs no second screen.
60
+
61
+ **Gap #555:** The kit's Stock screener is a report with filters and no export.
62
+
63
+ **Do:** A filter row above the table, the export at its end.
64
+
65
+ **Don't:** A table alone, its period fixed when it was built.
66
+
67
+ ## Put the table under every chart
68
+
69
+ <!-- rule: table-behind-the-chart -->
70
+
71
+ **Rule:** Show the table a chart was drawn from under it, on every report.
72
+
73
+ **Why:** A chart shows the shape of a change, not the amount it is read for.
74
+
75
+ **Do:** The months as bars, then the same months as rows.
76
+
77
+ **Don't:** The bars alone, every month estimated by eye.
@@ -8,7 +8,7 @@
8
8
 
9
9
  **Why:** Without a legend, readers need labels for unfamiliar glyphs.
10
10
 
11
- **Except:** [Earn a wordless button](https://ui.apli.tech/storybook/?path=/story/guidelines-button-labels--button-labels) sets the three tests an entry passes, and the list grows by decision. Today: `x` to close or dismiss, `copy` to copy to clipboard, `moreHorizontal` and `moreVertical` for an overflow menu, `chevronDown` and `chevronUp` to expand or collapse. The shell’s chrome — theme toggle, sidebar toggle, collapsed-rail links — is wordless by a separate decision.
11
+ **Except:** New entries require a decision and the three tests in [Earn a wordless button](https://ui.apli.tech/storybook/?path=/story/guidelines-button-labels--button-labels). Allowed: close, copy, overflow, expand, collapse and export. Theme toggles, sidebar toggles and collapsed-rail links have separate approval.
12
12
 
13
13
  **Do:** Show an overflow menu without words; keep the Settings label.
14
14
 
@@ -6,11 +6,11 @@
6
6
 
7
7
  **Rule:** Put key figures in a stat band, not in a card of parts.
8
8
 
9
- **Why:** A shared layout keeps values, changes and their comparison basis in the same place for each figure.
9
+ **Why:** A shared layout puts every figure's value, change and basis in the same place.
10
10
 
11
- **Do:** Use one card per figure by default, with the figure largest, an arrow, a signed change, and one comparison caption above the row.
11
+ **Do:** One card per figure: the figure largest, an arrow, a signed change, one caption above the row.
12
12
 
13
- **Don't:** Use heading-sized figures with a capitalised 10px status chip and no comparison basis.
13
+ **Don't:** Heading-sized figures, a capitalised 10px status chip, no comparison basis.
14
14
 
15
15
  ## Colour by meaning
16
16
 
@@ -20,11 +20,11 @@
20
20
 
21
21
  **Why:** Direction alone does not show whether a change is beneficial.
22
22
 
23
- **Except:** Omitted tone and explicit “neutral” keep the arrow and comparison but use no colour for unscored volume, reader-dependent meaning, or net cashflow already explained by income less cost.
23
+ **Except:** Omitted tone and explicit “neutral” keep the arrow and comparison without colour: unscored volume, reader-dependent meaning, net cashflow that income less cost already explains.
24
24
 
25
- **Do:** Use four verdicts: rising Income and Cost use opposite colours; falling Unclassified is green; undeclared Net cashflow stays grey.
25
+ **Do:** Four verdicts: rising Income and Cost take opposite colours; falling Unclassified is green; undeclared Net cashflow stays grey.
26
26
 
27
- **Don't:** Colour four figures by sign, making three rises green, praising cost growth, and showing the year’s cleanup as red.
27
+ **Don't:** Four figures coloured by sign: three rises green, cost growth praised, the year’s cleanup red.
28
28
 
29
29
  ## State the comparison basis
30
30
 
@@ -34,6 +34,8 @@
34
34
 
35
35
  **Why:** Percentages need a basis that readers can find.
36
36
 
37
- **Do:** Put the basis once in a caption referenced by each change, or beside an individual exception; keep the caption before and outside every figure.
37
+ **Except:** A comparison basis may be screen-reader-only when the visible control names the period. Use this exception for the Finance dashboard pattern.
38
38
 
39
- **Don't:** Use a title attribute, put the note below the tiles, place it inside the first figure, omit the basis, or nest captions.
39
+ **Do:** The basis once, in a caption each change references or beside one exception; the caption before and outside every figure.
40
+
41
+ **Don't:** A title attribute, the note below the tiles or inside the first figure, no basis, nested captions.
@@ -114,10 +114,10 @@
114
114
 
115
115
  <!-- rule: lede -->
116
116
 
117
- **Rule:** Keep the introduction to two sentences or fewer and add information the title does not give.
117
+ **Rule:** A page may open on its title alone; where there is an introduction, keep it to two sentences or fewer and add information the title does not give.
118
118
 
119
119
  **Why:** Repeating the title wastes the reader’s time.
120
120
 
121
121
  **Do:** Below “Payouts”, say “Payouts arrive within three business days.”
122
122
 
123
- **Don't:** Write “This is the payouts page” below “Payouts.”
123
+ **Don't:** Write “This is the payouts page” below “Payouts,” or keep a line there because the page looks bare without one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.104.0",
3
+ "version": "0.105.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -80,6 +80,9 @@
80
80
  --tap-clear-x: var(--tap-gap);
81
81
  --tap-clear-y: var(--tap-gap);
82
82
  }
83
+ .ui-toolbar--split .ui-filter-bar {
84
+ column-gap: calc(var(--tap-gap) + var(--space-1) + 1px) !important;
85
+ }
83
86
  .ui-filter-bar__chip {
84
87
  --tap-clear-x: 0px;
85
88
  --tap-clear-y: 0px;
@@ -175,6 +175,11 @@ export const iconOnlyAllowed = {
175
175
  moreVertical: 'overflow menu',
176
176
  chevronDown: 'expand or collapse',
177
177
  chevronUp: 'expand or collapse',
178
+ // Added by Artur's review of the Finance report, #505: a report's export is a
179
+ // standing offer sat next to the filters that narrow it, and the tray-and-arrow
180
+ // is the glyph a reader has already met doing this everywhere else. The entry
181
+ // is the decision — review did not infer it from the glyph looking obvious.
182
+ download: 'export or download the rows',
178
183
  };
179
184
 
180
185
  // The word a wordless control's name may open with. The list above says which
@@ -190,6 +195,11 @@ export const iconOnlyNames = {
190
195
  moreVertical: ['more'],
191
196
  chevronDown: ['expand', 'collapse', 'show', 'hide'],
192
197
  chevronUp: ['expand', 'collapse', 'show', 'hide'],
198
+ // The report's export, #505. Export is the word the kit's own call sites open
199
+ // with; download is the reader's other name for the same action, and the glyph
200
+ // depicts it, so a consumer naming it that way is not borrowing the tray for
201
+ // something else.
202
+ download: ['export', 'download'],
193
203
  };
194
204
 
195
205
  // What a glyph means when a component picks it for the reader rather than a
@@ -58,11 +58,20 @@ const figure = ({ label = '', value = '', caption = '', delta, trend = '' }, til
58
58
  // `basis` is the band's caption: what every change is measured against, said
59
59
  // once before the list the way a table's <caption> is, in every layout — under
60
60
  // a row of tiles it would read as a note on the last card.
61
+ //
62
+ // `basisId` is the other way to answer the same question: the id of a statement
63
+ // the caller has already placed before the figures, which each change points at
64
+ // instead. It is for a band whose basis is stated by something beside it — a
65
+ // period control over a dashboard's figures selects the window every change is
66
+ // measured against, and a caption under it would say that again. React's
67
+ // StatBand has carried it since the period showcase; this is the factory's half.
68
+ // `basis` wins when both are passed, because a band that draws its own caption
69
+ // has said it.
61
70
  // why: docs/components.md#stat-bands
62
- export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
71
+ export function statBand({ stats = [], variant = 'tiles', basis = '', basisId: shared = '', label, id } = {}) {
63
72
  const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
64
73
  const base = id ? esc(id) : `ui-stats-${++seq}`;
65
- const basisId = basis ? `${base}-basis` : '';
74
+ const basisId = basis ? `${base}-basis` : (shared ? esc(shared) : '');
66
75
  const cls = cx('ui-stats', `ui-stats--${v}`, v === 'band' && 'ui-card');
67
76
  const named = label ? ` role="group" aria-label="${esc(label)}"` : '';
68
77
  const items = stats.map((s) => figure(s, v === 'tiles', basisId)).join('');
@@ -91,7 +91,6 @@
91
91
  line-height: var(--leading-snug);
92
92
  margin-bottom: var(--space-5);
93
93
  }
94
-
95
94
  /* Setting rows: label + control, divided by a hairline */
96
95
  .ui-card__row {
97
96
  display: flex;
@@ -1,16 +1,34 @@
1
1
  .ui-filter-bar { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
2
2
  .ui-filter-bar__legend { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); }
3
- .ui-filter-bar__chip { display: inline-flex; align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: var(--space-1); margin: 0; min-width: 0; }
3
+ /* A chip is a control, so it is painted like the controls beside it: the field
4
+ surface and the same edge a button takes. Left transparent it read as the page
5
+ showing through a switched-off box — the complaint behind #505's review. */
6
+ .ui-filter-bar__chip {
7
+ display: inline-flex; align-items: center; min-width: 0; margin: 0;
8
+ border: 1px solid var(--control-edge); border-radius: var(--radius-sm); padding: var(--space-1);
9
+ /* The chip holds two focus targets of its own — the trigger and the remove
10
+ button — so it is a painted container, not a control that inherits the gap
11
+ outside it. It joins the shared recomposition in tokens.css beside .ui-seg,
12
+ which is also what hands an inline code chip the page from here. */
13
+ background: var(--surface);
14
+ }
4
15
  .ui-filter-bar__chip .ui-dropdown__trigger { border: 0; background: transparent; padding: var(--space-1) var(--space-2); }
5
16
  .ui-filter-bar__remove { border: 0; background: transparent; color: var(--text); font: inherit; text-align: center; font-size: var(--text-lg); padding: 0; min-width: 24px; min-height: 24px; border-radius: var(--radius-xs); cursor: pointer; }
6
17
  .ui-filter-bar__remove:focus-visible,
7
18
  .ui-filter-bar:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
8
19
  .ui-filter-bar button:disabled { /* muted-ink: state — disabled filters */ color: var(--disabled-ink); cursor: not-allowed; }
9
20
 
21
+ /* The chip's value takes its button's own ink, so a disabled chip keeps the
22
+ disabled ink it inherits instead of the value ink the enabled rules set. */
10
23
  .ui-filter-bar .ui-dropdown__value { color: inherit; }
24
+ .ui-filter-bar button:enabled .ui-dropdown__value { color: var(--strong); }
11
25
  /* A chip with nothing chosen prints the field's name in the slot the value will
12
- take. `:enabled` leaves a disabled chip its own ink. why: guidelines/labels-and-titles.md */
13
- .ui-filter-bar button:enabled .ui-dropdown__value.is-placeholder { /* muted-ink: placeholder — a filter chip with nothing chosen */ color: var(--muted); }
26
+ take — in the page's reading ink, not --muted. --disabled-ink IS --muted, so a
27
+ muted name on an offered filter said "switched off" in the one ink the kit
28
+ reserves for that; the pick keeps the heavier, darker value ink, so a chosen
29
+ chip and an empty one are still told apart. `:enabled` leaves a disabled chip
30
+ its own ink. why: guidelines/labels-and-titles.md, #505 */
31
+ .ui-filter-bar button:enabled .ui-dropdown__value.is-placeholder { color: var(--text); font-weight: var(--weight-normal); }
14
32
 
15
33
  /* Shut, a panel is the trigger's width, never the 240px floor a standalone
16
34
  dropdown gets: it is laid out while hidden, so a wider one widened the page
@@ -119,7 +119,7 @@
119
119
  resting step. */
120
120
  /* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
121
121
  the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
122
- stories/apps/shell-states.test.js:1704 `a resting glyph is dimmer but still legible`. */
122
+ stories/apps/shell-states.test.js:1705 `a resting glyph is dimmer but still legible`. */
123
123
  .ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
124
124
 
125
125
  /* A destructive row of the rail is a navigation row and rests like one; the danger
@@ -437,10 +437,9 @@
437
437
  letter-spacing: var(--tracking-tight);
438
438
  margin-bottom: 8px;
439
439
  }
440
- /* The head keeps the kit's second step below it whether or not a lede is there to
441
- carry it: with no lede the title's own margin takes the step instead. */
442
- .ui-app__main h1:not(.ui-card h1):not(:has(+ .ui-app__sub)) { margin-bottom: var(--space-8); }
443
440
  .ui-app__sub { color: var(--text); font-size: calc(var(--text-base) + 0.5px); line-height: 1.6; max-width: var(--prose-body); margin-bottom: var(--space-8); }
441
+ /* A page may open on its title alone; then the title carries the head's step. */
442
+ .ui-app__main > h1:not(:has(+ .ui-app__sub)) { margin-bottom: var(--space-8); }
444
443
  .ui-app__body { display: flex; flex-direction: column; gap: var(--space-5); }
445
444
  /* A flex child's min-width is its content by default, so one wide block — a
446
445
  ledger, a code sample — pushes the whole page sideways instead of scrolling
@@ -459,14 +458,48 @@
459
458
  of what you typed. min-width:0 keeps an input's automatic minimum (its `size`
460
459
  attribute, ~20 characters) from setting a floor well above the basis. */
461
460
  .ui-toolbar { display: flex; flex-wrap: wrap; gap: 10px; }
461
+ /* A control row belongs to the block it narrows rather than standing beside it,
462
+ so it sits closer than .ui-app__body's gap between two blocks that are each
463
+ their own subject. Not on the last row: there the gap is the page's bottom
464
+ and nothing is being narrowed. why: Artur's review of the Finance report, #505 */
465
+ .ui-app__body > .ui-toolbar:not(:last-child) { margin-bottom: calc(var(--space-3) - var(--space-5)); }
462
466
  .ui-toolbar > .ui-input,
463
467
  .ui-toolbar > .ui-input-group { flex: 1 1 6rem; min-width: 0; }
468
+ /* A row that offers the block two unrelated things — what to narrow it by, and
469
+ what to do with it — puts the second at the far end instead of beside the
470
+ first. The auto margin is the same one the rail's fold toggle rides to the end
471
+ of the brand row, so the gap between the two groups is whatever the line has
472
+ left and the row still wraps on its own. Above the phone step a wrapped line
473
+ keeps the end it was sent to; below it there is no line, and the rule in the
474
+ query further down takes the margin off.
475
+ why: Artur's review of the Finance report, #505 */
476
+ .ui-toolbar--split > :last-child { margin-inline-start: auto; }
464
477
  /* At one column the 6rem basis stops being a floor and becomes the field's size:
465
478
  a row with two narrow chips beside it keeps all three on the line and cuts the
466
479
  field's own placeholder off. why: docs/components.md#a-toolbar-at-one-column */
467
480
  @media (max-width: 560px) {
468
481
  .ui-toolbar > .ui-input,
469
482
  .ui-toolbar > .ui-input-group { flex-basis: 100%; }
483
+ /* The end control stays IN the row at this width rather than dropping under it.
484
+ A flex line breaks on hypothetical sizes, so a group whose content is wider
485
+ than the line takes that line alone however much it shrinks afterwards: at
486
+ 390 the report's filter group asks for about 340px of chips and clear in a
487
+ 284px row, so the 36px export went to a line of its own, left-aligned under
488
+ the clear button — which is the reading `--split` exists to prevent, drawn
489
+ vertically. Given a basis the group no longer claims the whole line, the end
490
+ control fits beside it, and the group grows into what is left. 60% rather
491
+ than half because at 560 half still left room for the group beside the
492
+ period strip, which put the export back under the row.
493
+
494
+ The auto margin goes: with the group grown there is no slack for it to take,
495
+ and on the line it did have it left the control against the right edge of an
496
+ otherwise empty row — a mark that came loose rather than what to do with the
497
+ block, which is what Artur marked on round r31. `align-self` is what keeps
498
+ the control its own height beside a group two lines tall; it sits at the
499
+ row's top end, the corner it keeps above this step.
500
+ why: Artur's review of the Finance report, rounds r31 and r34, #505 */
501
+ .ui-toolbar--split > :nth-last-child(2) { flex: 1 1 60%; min-width: 0; }
502
+ .ui-toolbar--split > :last-child { margin-inline-start: 0; align-self: start; }
470
503
  }
471
504
 
472
505
  /* -- The narrow rail ------------------------------------------------------ */
@@ -139,6 +139,20 @@ dd.ui-stat__caption { contain: inline-size; }
139
139
  @container (width <= 28rem) {
140
140
  .ui-stats__list > .ui-stat { flex-basis: 100%; }
141
141
  }
142
+
143
+ /* Stacking runs out before a phone does. One figure to a row, a value can still
144
+ be wider than the box holding it — it may neither wrap nor truncate — so the
145
+ band pushed its column sideways: at 320, 15px past the shell's 214px column
146
+ and 19px past a specimen cell. Below its own width the value takes the rank
147
+ under it. A width each, because a tile pads its figure and an open band does
148
+ not, so they stop fitting 16px apart; what a figure longer than the kit's own
149
+ still does is in docs/components.md#stat-bands. #505 */
150
+ @container (width <= 14rem) {
151
+ .ui-stats:not(.ui-stats--open) .ui-stats__list > .ui-stat .ui-stat__value { font-size: var(--text-xl); }
152
+ }
153
+ @container (width <= 15rem) {
154
+ .ui-stats--open .ui-stats__list > .ui-stat .ui-stat__value { font-size: var(--text-2xl); }
155
+ }
142
156
  @container (32rem < width <= 66rem) {
143
157
  .ui-stats--open .ui-stats__list:has(> .ui-stat:nth-child(4):last-child) > .ui-stat { flex-basis: 34%; }
144
158
  }
@@ -1,6 +1,6 @@
1
1
  /* ============================================================================
2
2
  * Table — .ui-table (hairline rows, opaque base surface)
3
- * Row modifiers: .is-dead (revoked/struck), right-aligned .ui-table__act
3
+ * Row modifiers: .is-dead (revoked/struck), .is-target (landed on), right-aligned .ui-table__act
4
4
  * ========================================================================== */
5
5
 
6
6
  .ui-table {
@@ -76,7 +76,7 @@
76
76
  /* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
77
77
  * --dense --space-3 across, --space-2 down — one step in from the base table
78
78
  * --zebra legacy spacing and 2px header rule; the base-surface rule below removes stripes
79
- * Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
79
+ * Cell helpers: .ui-table__num (right, tabular), .ui-table__code, .ui-table__pair.
80
80
  * why: docs/foundations.md#spacing-and-rhythm
81
81
  * held by: stories/table-rhythm.test.js */
82
82
  .ui-table--dense th { padding: 0 var(--space-3) var(--space-2); }
@@ -116,6 +116,22 @@
116
116
  color: var(--strong);
117
117
  text-decoration: underline;
118
118
  }
119
+ /* One exception, and it is the way through: a reference that opens another screen at
120
+ this row. The rule above was written to keep a LEDGER's identifiers out of the accent
121
+ — a whole column of them is an accent with no job (guidelines/density-and-accents.md,
122
+ give-accents-a-job) — but a dashboard's exceptions are three rows, and there the link
123
+ is how a reader reaches the row's own report. Left in the row's ink it was the plain
124
+ cell beside it to the pixel, resolving only on hover, which a phone does not have.
125
+ .ui-table__pair is the cell that carries a reference and its state, so the exception is
126
+ as narrow as that cell. The ink is base.css's own `a { color: var(--accent) }`, at
127
+ 7.34:1 in light and 5.53:1 in dark on the table's ground.
128
+ why: Artur, round r34; guidelines/dashboards-and-reports.md#link-each-reference-to-its-row
129
+ held by: src/styles/table.test.js */
130
+ .ui-table :where(td, th) .ui-table__pair a:not(.ui-btn):not(.ui-identity) { color: var(--accent); }
131
+ .ui-table :where(td, th) .ui-table__pair a:not(.ui-btn):not(.ui-identity):hover {
132
+ color: var(--accent);
133
+ text-decoration: underline;
134
+ }
119
135
  /* A plain link — one carrying no class — is the link these rules are about. An inline-block
120
136
  box, so a title link that wraps paints one ring instead of one per line, and the kit's small
121
137
  corner for that ring to follow. Anchors the kit already styles (`.ui-btn`, `.ui-identity`,
@@ -163,6 +179,15 @@
163
179
  * row reads as one row. */
164
180
  .ui-table :is(th, td).ui-table__num--strong { color: var(--strong); font-weight: var(--weight-semibold); }
165
181
  .ui-table__code { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
182
+ /* Two things in one cell — a row's own name and the state of it — side by side
183
+ * while the column has room and stacked when it does not. The wrap comes from flex
184
+ * because a dense cell is nowrap, and the column is then only as wide as the wider
185
+ * of the two: a status that had a column of its own was the first thing a phone cut
186
+ * off, and in here it is the last. The class goes on a span inside the cell, never
187
+ * on the cell, which would leave the table box model. A row carrying a paragraph takes
188
+ * .ui-table--stack below instead, which drops the head; a pair keeps every head at every
189
+ * width, which is what a three-value glance row is read by. why: #505 */
190
+ .ui-table__pair { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); }
166
191
 
167
192
  /* The empty state (.ui-empty) — shown when a table/list has no rows — now
168
193
  lives in styles/empty.css as a standalone component (illustration support). */
@@ -182,6 +207,41 @@
182
207
  }
183
208
  .ui-table tbody tr:hover { outline: 1px solid var(--border-strong); outline-offset: -1px; }
184
209
 
210
+ /* The row a reference landed a reader on. The mark is a GROUND, not a line: an outline
211
+ says the keyboard is here (Artur, r34), and this one persists — it answers which row the
212
+ reader came for, so it stays until the next arrival and must not wear the picture focus
213
+ draws. It took --ring-scroll until #505's review found a landed row still outlined in the
214
+ accent after focus had moved on, with a second row beside it wearing the same box.
215
+ The ground and the ring paint together on arrival: `landOnRow()` focuses the row, so a
216
+ reader must see both at once, not the ring now and the ground only after they tab away.
217
+
218
+ The wash is the accent over the table's own ground: the family `tbody tr:hover` already
219
+ uses for "this row", one step up from its 8% so the two are told apart. 12% is the
220
+ strongest step that keeps an accent link inside the row above 4.5:1 — 4.60:1 at the worst
221
+ of the eight accent/theme cells, against 4.45:1 at 14%. It paints cells rather than the
222
+ row, because the hover and zebra rules paint cells and would otherwise cover it; the two
223
+ hover selectors are repeated with .is-target so a pointer passing over the row does not
224
+ take its mark off.
225
+ why: Artur, round r34; guidelines/dashboards-and-reports.md#link-each-reference-to-its-row
226
+ held by: stories/dashboard-report-refs.test.js, for landOnRow() marking the right row and
227
+ only the right row; stories/contrast.test.js, for the wash's contrast, now that
228
+ stories/components/Table.stories.js:FinanceData renders a row wearing the mark at rest.
229
+ Neither gate reads paint against focus: the ground and the ring share no selector and no
230
+ condition, which is why one cannot switch the other off, and that is proved by the PR's
231
+ browser captures rather than by a gate. PR #552 code review, finding F6. */
232
+ .ui-table tbody tr.is-target > :is(td, th),
233
+ .ui-table--hover tbody tr.is-target:hover > :is(td, th),
234
+ .ui-table--zebra.ui-table--hover tbody tr.is-target:hover > :is(td, th) {
235
+ background: color-mix(in srgb, var(--accent) 12%, transparent); /* code-bg: inherit — this translucent wash leaves the chip on its opaque ground. */
236
+ }
237
+
238
+ /* A focused row draws the shared ring inward, like its containing scroll region.
239
+ why: docs/foundations.md#the-focus-ring */
240
+ .ui-table tbody tr:focus-visible {
241
+ outline: var(--ring-scroll);
242
+ outline-offset: var(--ring-scroll-offset);
243
+ }
244
+
185
245
  /* The end-cell inset above gives a row's highlight room; the bleed is what stops the grid
186
246
  paying for it. The table hangs out by the same --space-3, into the card's 20-24px
187
247
  padding and never past it, so the columns start on the card's own text edge and the
@@ -140,6 +140,9 @@
140
140
  /* Chips open both ways: the row wraps, and the button at the end of it is a
141
141
  target like the chips are. */
142
142
  .ui-filter-bar { gap: var(--tap-gap) !important; --tap-clear-x: var(--tap-gap); --tap-clear-y: var(--tap-gap); }
143
+ /* In a split toolbar the rail row beside the report sits on the same line as
144
+ Clear all. One step plus 1px keeps the button's layer off that row. */
145
+ .ui-toolbar--split .ui-filter-bar { column-gap: calc(var(--tap-gap) + var(--space-1) + 1px) !important; }
143
146
 
144
147
  /* Inside a chip there is nothing to open: parting the value from its remove
145
148
  mark would draw two controls where the reader sees one. Named on the
@@ -528,6 +528,7 @@
528
528
  .ui-drawer__panel,
529
529
  .ui-dropdown__panel,
530
530
  .ui-fbcomposer,
531
+ .ui-filter-bar__chip,
531
532
  .ui-footer--app,
532
533
  .ui-seg,
533
534
  .ui-snippet,