@lotics/ui 44.10.0 → 44.10.1
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/MIGRATION.md +83 -4
- package/docs/reviewing.md +47 -11
- package/package.json +1 -1
- package/src/file_row.tsx +10 -1
- package/src/ledger.tsx +37 -5
- package/src/matrix.tsx +14 -3
- package/src/option_list.tsx +15 -1
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,79 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
**`FileRow`'s `leading` slot is a SIBLING of the press door, not inside it.** No API change;
|
|
8
|
+
a row with `onPress` now has a slightly smaller press target, excluding the leading mark.
|
|
9
|
+
|
|
10
|
+
`trailing`'s own docstring already stated the rule — *"an independently-interactive SIBLING:
|
|
11
|
+
never swallowed by the row press (a button never nests in the door button)"* — while `leading`
|
|
12
|
+
recommended *"a selection checkbox"* and rendered inside the `Pressable`. So the documented
|
|
13
|
+
usage produced an interactive control nested in a `button`: invalid ARIA, and one press with two
|
|
14
|
+
owners, since ticking the box also opened the file. A slot the docs tell you to put a control in
|
|
15
|
+
cannot be inside the control.
|
|
16
|
+
|
|
17
|
+
## 44.9.3 — an option's description stops being erased, and a pivot's headers rejoin the ramp
|
|
18
|
+
|
|
19
|
+
**`OptionList` no longer suppresses `getOptionDescription` when `renderOptionContent` is set.**
|
|
20
|
+
The two are independent facts about an option — how its value renders, and a subtitle under it —
|
|
21
|
+
and one was erasing the other. A caller who wanted both had one slot for two things, so they put
|
|
22
|
+
the subtitle inside the custom render; `Select` and `InlineSelect` then reuse that render as the
|
|
23
|
+
RESTING trigger display, which turned a gloss meant for the open menu into a two-line control on
|
|
24
|
+
every read (measured at 55px against 40px for every other editor in the same table). Pass no
|
|
25
|
+
`getOptionDescription` if you want no subtitle. The documented `renderSelected` →
|
|
26
|
+
`renderOptionContent` → `label` fallback is unchanged; it was never the defect.
|
|
27
|
+
|
|
28
|
+
**`Matrix`'s column headers are `sm` / `muted` / `medium`, matching `SortHeader`.** They were
|
|
29
|
+
`xs` / regular — which composition.md already names as the failure: *"a column header is a LABEL
|
|
30
|
+
at the body size… dropped to `xs` and lightened past `muted` it stops being legible and starts
|
|
31
|
+
reading as a caption that lost its value."* A screen carrying both a `Table` and a `Matrix`
|
|
32
|
+
rendered the same structural role two ways.
|
|
33
|
+
|
|
34
|
+
## 44.9.3 — the rules probe could not see a rule
|
|
35
|
+
|
|
36
|
+
**`docs/reviewing.md`'s probe 3 collector missed border-drawn rules and counted transparent
|
|
37
|
+
spacers.** Docs only, no code change — but this is the method app authors run, so it shipped a
|
|
38
|
+
blind spot: it selected on "thin and wide" with no paint check, which counts a zero-height flex
|
|
39
|
+
spacer as a rule and misses a band drawn as a `borderBottom` on a full-height header row. A
|
|
40
|
+
surface with a visible rule under its column headers therefore reported **zero**, and zero reads
|
|
41
|
+
as a clean result rather than a blind probe.
|
|
42
|
+
|
|
43
|
+
The snippet now requires actual paint, accepts a single-sided border on a box of any height,
|
|
44
|
+
excludes four-sided boxes (a control's own edge is not a rule), reads the line's y from the border
|
|
45
|
+
edge rather than the box origin, and reports what each rule SEPARATES — which is what probe 3
|
|
46
|
+
actually asks.
|
|
47
|
+
|
|
48
|
+
## 44.9.2 — a pivot's figures actually align, and a ledger row keeps its own name
|
|
49
|
+
|
|
50
|
+
**`Matrix.Grid`'s cell stretches its figure instead of centring it, so 44.9.1's right-alignment
|
|
51
|
+
actually applies.** A `Text` centred by its parent shrink-wraps, so the `align="right"` added in
|
|
52
|
+
44.9.1 had nothing to align against and the value cells never moved — while the headers and totals,
|
|
53
|
+
which are not centred, did. Measured on a real pivot as shipped in 44.9.1, figures in one column
|
|
54
|
+
ended across a **47px** spread; with the cell stretched, **0**. `Matrix.Header`'s column cell also
|
|
55
|
+
gains a 2px right padding, matching the inset the cell's own border imposes, so a header and the
|
|
56
|
+
numbers beneath it end on one x rather than two pixels apart.
|
|
57
|
+
|
|
58
|
+
**`Ledger` measures itself and `LedgerRow` stops rendering `meta` in a row too tight for it.**
|
|
59
|
+
No API change; a narrow ledger simply shows fewer captions.
|
|
60
|
+
|
|
61
|
+
44.9.1 made the caption yield to the label first, which was the right ORDER and not a floor:
|
|
62
|
+
shrinking indefinitely still starves the identity. Measured at 375, a 30-character caption
|
|
63
|
+
wanted 184px of a ~250px text budget and clipped anyway, ellipsising a 74px label at 71px. The
|
|
64
|
+
row now drops the caption entirely below the width where a label, a caption and a figure can
|
|
65
|
+
all be seated (`FLEX_MIN_WIDTH` for the identity, ~100 for the money column, ~80 before a
|
|
66
|
+
caption stops being worth rendering).
|
|
67
|
+
|
|
68
|
+
Measured across the three states in a 180px row, the label kept 31px, then 55px, then 74px of
|
|
69
|
+
the 77 it needs.
|
|
70
|
+
|
|
71
|
+
`Ledger` measures its own box via `onLayout`, the way `Table` / `DetailTable` / `Breakdown`
|
|
72
|
+
already do — not the window, and once for the whole ledger rather than per row. A ledger in a
|
|
73
|
+
narrow column on a wide screen is exactly the case a window-sized answer gets wrong. Two
|
|
74
|
+
separate app authors had already worked around this by dropping `meta` at small widths in their
|
|
75
|
+
own code; that is the component's job.
|
|
76
|
+
|
|
77
|
+
`LedgerBasis` is unaffected — it stacks its caption UNDER the label, so the two never compete
|
|
78
|
+
for one line.
|
|
79
|
+
|
|
7
80
|
## 44.9.1 — a pivot's figures align, and a ledger row stops losing its own name
|
|
8
81
|
|
|
9
82
|
No API is removed and nothing fails to compile. Three renders change.
|
|
@@ -11,10 +84,16 @@ No API is removed and nothing fails to compile. Three renders change.
|
|
|
11
84
|
**`Matrix` right-aligns its figures instead of centring them.** Every value, column
|
|
12
85
|
header and total was `align="center"` while the cells also set `tabular` — and tabular
|
|
13
86
|
figures exist so digits line up in a column, which centring throws away. Measured on a
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
centred
|
|
87
|
+
**This was only half the fix — see 44.9.2.** `Matrix.Grid`'s cell sets `alignItems: "center"`,
|
|
88
|
+
which makes the figure inside SHRINK-WRAP, and `align="right"` on a box exactly as wide as its
|
|
89
|
+
own glyphs does nothing. So this release right-aligned the headers and totals (which carry no
|
|
90
|
+
such centring) and left the value cells centred — worse than either end state, since a figure
|
|
91
|
+
could then sit up to 45px left of its own column header's right edge. The measurement quoted
|
|
92
|
+
below counted distinct right edges across ALL figures rather than the spread WITHIN a column,
|
|
93
|
+
which is why it read as an improvement. Measured on a real pivot: centred, 24 figures painted
|
|
94
|
+
across 14 distinct right edges; right-aligned, 12. No `align` prop came with this: a matrix is
|
|
95
|
+
one measure across two dimensions, so its cells cannot want different alignments, and a prop for
|
|
96
|
+
a centred case nobody has is surface without a consumer.
|
|
18
97
|
|
|
19
98
|
**`Matrix` row labels carry default ink, not `muted`.** A pivot whose rows are entities
|
|
20
99
|
rendered every entity name a step quieter than the numbers about it, inverting how the
|
package/docs/reviewing.md
CHANGED
|
@@ -187,7 +187,9 @@ rule behind it lives in the area doc named beside it; this file never restates o
|
|
|
187
187
|
→ [composition.md](./composition.md) §"Color discipline", §"Buttons & action labels", §"Every number is a door", §"Hover grammar".
|
|
188
188
|
|
|
189
189
|
### 3. Rules — ask what each one SEPARATES
|
|
190
|
-
**Collect** every border/divider with weight, colour and width
|
|
190
|
+
**Collect** every border/divider with weight, colour and width — including the ones drawn as a
|
|
191
|
+
border on a box of ordinary height, which is how a column band is usually painted. A collector that
|
|
192
|
+
matches only thin boxes reports zero rules on a surface that has one, and zero reads as clean.
|
|
191
193
|
|
|
192
194
|
The test is not how big the break is. A rule between two rows divides **like from like** —
|
|
193
195
|
whitespace says that just as well, which is why a register can drop every internal rule and read
|
|
@@ -441,25 +443,59 @@ that has none, or a clean tally on a screen with an untracked component still on
|
|
|
441
443
|
|
|
442
444
|
### Separator beats — is the biggest boundary the widest?
|
|
443
445
|
|
|
446
|
+
**A rule is a painted LINE, and it is drawn two ways.** Selecting "thin and wide" alone gets both
|
|
447
|
+
halves wrong, and both wrongs are silent: a transparent zero-height flex spacer matches it and is
|
|
448
|
+
counted, while a border on a box of ordinary height does not and is missed. The second is the one
|
|
449
|
+
that bites — a component that draws its column band as a `borderBottom` on a full-height header row
|
|
450
|
+
then reports **zero rules on a surface that visibly has one**, and a count of `0` reads as a clean
|
|
451
|
+
result rather than a blind probe.
|
|
452
|
+
|
|
444
453
|
```js
|
|
445
454
|
() => {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
455
|
+
const CLEAR = v => !v || v === 'rgba(0, 0, 0, 0)' || v === 'transparent';
|
|
456
|
+
const lineOf = e => {
|
|
457
|
+
const r = e.getBoundingClientRect(), cs = getComputedStyle(e);
|
|
458
|
+
if (!(r.width > 200 && e.checkVisibility?.())) return null;
|
|
459
|
+
if (r.height <= 1.5) { // a hairline BOX
|
|
460
|
+
if (!CLEAR(cs.backgroundColor)) return { y: r.y, paint: cs.backgroundColor };
|
|
461
|
+
for (const s of ['Top', 'Bottom']) // …or a hairline drawn as a border
|
|
462
|
+
if (parseFloat(cs[`border${s}Width`]) > 0 && !CLEAR(cs[`border${s}Color`]))
|
|
463
|
+
return { y: r.y, paint: cs[`border${s}Color`] };
|
|
464
|
+
return null; // painted nothing: a spacer, not a rule
|
|
465
|
+
}
|
|
466
|
+
const sides = ['Top', 'Bottom'].filter(s =>
|
|
467
|
+
parseFloat(cs[`border${s}Width`]) > 0 && !CLEAR(cs[`border${s}Color`]));
|
|
468
|
+
const boxed = ['Left', 'Right'].every(s =>
|
|
469
|
+
parseFloat(cs[`border${s}Width`]) > 0 && !CLEAR(cs[`border${s}Color`])) && sides.length === 2;
|
|
470
|
+
if (boxed || sides.length !== 1) return null; // a control's own edge is not a rule
|
|
471
|
+
// The LINE's y is the border's edge, never the box's origin.
|
|
472
|
+
return { y: sides[0] === 'Top' ? r.y : r.bottom, paint: cs[`border${sides[0]}Color`] };
|
|
473
|
+
};
|
|
474
|
+
const rules = [...document.querySelectorAll('div,hr')]
|
|
475
|
+
.map(lineOf).filter(Boolean)
|
|
476
|
+
.map(l => ({ y: Math.round(l.y), paint: l.paint }))
|
|
477
|
+
.sort((a, b) => a.y - b.y);
|
|
449
478
|
const blocks = [...document.querySelectorAll('*')]
|
|
450
479
|
.filter(e => e.children.length === 0 && e.textContent.trim())
|
|
451
|
-
.map(e => { const r = e.getBoundingClientRect(); return { y: Math.round(r.y), b: Math.round(r.bottom) }; });
|
|
452
|
-
return rules.map(y => ({
|
|
453
|
-
rule: y,
|
|
454
|
-
above: y - Math.max(...blocks.filter(o => o.b <= y).map(o => o.b), -Infinity),
|
|
455
|
-
below: Math.min(...blocks.filter(o => o.y >= y).map(o => o.y), Infinity) - y,
|
|
480
|
+
.map(e => { const r = e.getBoundingClientRect(); return { y: Math.round(r.y), b: Math.round(r.bottom), t: e.textContent.trim().slice(0, 24) }; });
|
|
481
|
+
return rules.map(({ y, paint }) => ({
|
|
482
|
+
rule: y, paint,
|
|
483
|
+
above: y - Math.max(...blocks.filter(o => o.b <= y + 1).map(o => o.b), -Infinity),
|
|
484
|
+
below: Math.min(...blocks.filter(o => o.y >= y - 1).map(o => o.y), Infinity) - y,
|
|
485
|
+
separates: [blocks.filter(o => o.b <= y + 1).sort((a, c) => c.b - a.b)[0]?.t,
|
|
486
|
+
blocks.filter(o => o.y >= y - 1).sort((a, c) => a.y - c.y)[0]?.t].join(' | '),
|
|
456
487
|
}));
|
|
457
488
|
}
|
|
458
489
|
```
|
|
459
490
|
|
|
491
|
+
Read `separates` first — probe 3 asks what each rule DIVIDES, and a rule whose two neighbours are
|
|
492
|
+
the same kind of thing is the one to delete however important the break feels.
|
|
493
|
+
|
|
460
494
|
Unequal `above`/`below` on one rule, or a rule with a visibly tighter beat than its neighbours, is
|
|
461
|
-
the finding. A hand-rolled `Divider` sitting in a parent's uniform `gap` next to a
|
|
462
|
-
is the usual cause.
|
|
495
|
+
the other finding. A hand-rolled `Divider` sitting in a parent's uniform `gap` next to a
|
|
496
|
+
`SectionStack` is the usual cause. Asymmetry is not automatically wrong: a rule that BELONGS to
|
|
497
|
+
the element above it sits tight under it and loose before the next, and that gap is what says
|
|
498
|
+
which side owns it — the finding is asymmetry nobody chose.
|
|
463
499
|
|
|
464
500
|
### Clipping, overflow, collision
|
|
465
501
|
|
package/package.json
CHANGED
package/src/file_row.tsx
CHANGED
|
@@ -40,6 +40,14 @@ export interface FileRowProps {
|
|
|
40
40
|
* Leading slot, BEFORE the badge — a `DiffMark` saying what happened to this
|
|
41
41
|
* document, a selection checkbox, a status dot.
|
|
42
42
|
*
|
|
43
|
+
* A SIBLING of the row's press door, like `trailing` — not inside it. It used
|
|
44
|
+
* to render within the `Pressable`, which made the documented checkbox an
|
|
45
|
+
* interactive control nested in a `button`: invalid ARIA, and two owners for
|
|
46
|
+
* one press, since ticking the box also opened the file. The cost of the fix
|
|
47
|
+
* is that the mark itself is no longer part of the door; that is the correct
|
|
48
|
+
* trade, because a slot the docs tell you to put a control in cannot be
|
|
49
|
+
* inside the control.
|
|
50
|
+
*
|
|
43
51
|
* It sits outside the badge rather than on it because it must line up DOWN
|
|
44
52
|
* the list: a mark drawn over a 30px badge moves with the badge's size, and
|
|
45
53
|
* `sm` and `md` rows would stop agreeing. Give every row the same slot,
|
|
@@ -91,7 +99,6 @@ export function FileRow({
|
|
|
91
99
|
|
|
92
100
|
const content = (
|
|
93
101
|
<>
|
|
94
|
-
{leading}
|
|
95
102
|
<FileBadge size={md ? 38 : 30} mimeType={mimeType} placeholder={placeholder} isTemplate={isTemplate} />
|
|
96
103
|
<View style={styles.text}>
|
|
97
104
|
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
@@ -114,6 +121,7 @@ export function FileRow({
|
|
|
114
121
|
if (!onPress) {
|
|
115
122
|
return (
|
|
116
123
|
<View style={[styles.row, md && styles.rowMd]}>
|
|
124
|
+
{leading}
|
|
117
125
|
{content}
|
|
118
126
|
{trailing}
|
|
119
127
|
</View>
|
|
@@ -130,6 +138,7 @@ export function FileRow({
|
|
|
130
138
|
]}
|
|
131
139
|
{...mouseProps}
|
|
132
140
|
>
|
|
141
|
+
{leading}
|
|
133
142
|
<Pressable
|
|
134
143
|
onPress={onPress}
|
|
135
144
|
{...focusProps}
|
package/src/ledger.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
1
|
+
import { createContext, useContext, useState, type ReactNode } from "react";
|
|
2
2
|
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
4
|
import { Divider } from "./divider";
|
|
@@ -31,8 +31,17 @@ import { Text } from "./text";
|
|
|
31
31
|
// <LedgerTotal label="Outstanding" value={due} tone="danger" zeroLabel="Paid in full" />
|
|
32
32
|
// </Ledger>
|
|
33
33
|
|
|
34
|
+
/** Below this the row cannot seat a label, a caption and a figure at once.
|
|
35
|
+
* Derived, not chosen: the identity needs a flex column's usable floor
|
|
36
|
+
* (`FLEX_MIN_WIDTH`, 120), a money column runs ~100, and a caption is not worth
|
|
37
|
+
* rendering under ~80 — plus the row's own gaps. Under that the caption is
|
|
38
|
+
* taking room from the one string the reader needs. */
|
|
39
|
+
const META_MIN_ROW_WIDTH = 320;
|
|
40
|
+
|
|
34
41
|
interface LedgerContextValue {
|
|
35
42
|
format: (n: number) => string;
|
|
43
|
+
/** The row is too tight to carry a caption as well — see `META_MIN_ROW_WIDTH`. */
|
|
44
|
+
dropMeta: boolean;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
const LedgerContext = createContext<LedgerContextValue | null>(null);
|
|
@@ -61,9 +70,25 @@ export interface LedgerProps {
|
|
|
61
70
|
|
|
62
71
|
export function Ledger(props: LedgerProps) {
|
|
63
72
|
const { formatValue, children, style } = props;
|
|
73
|
+
// MEASURES ITSELF, like `Table`/`DetailTable`/`Breakdown` — not the window,
|
|
74
|
+
// and not per row. A ledger in a narrow column on a wide screen is the case a
|
|
75
|
+
// window-sized answer gets wrong, and one `onLayout` on the container serves
|
|
76
|
+
// every row in it.
|
|
77
|
+
//
|
|
78
|
+
// 0 until first layout, which reads as "not yet too narrow" — a caption that
|
|
79
|
+
// appears on the second frame is a far smaller wrong than one that flashes
|
|
80
|
+
// away, and a ledger wide enough to matter answers on that frame anyway.
|
|
81
|
+
const [width, setWidth] = useState(0);
|
|
64
82
|
return (
|
|
65
|
-
<LedgerContext.Provider
|
|
66
|
-
|
|
83
|
+
<LedgerContext.Provider
|
|
84
|
+
value={{ format: formatValue, dropMeta: width > 0 && width < META_MIN_ROW_WIDTH }}
|
|
85
|
+
>
|
|
86
|
+
<View
|
|
87
|
+
style={[styles.ledger, style]}
|
|
88
|
+
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</View>
|
|
67
92
|
</LedgerContext.Provider>
|
|
68
93
|
);
|
|
69
94
|
}
|
|
@@ -230,13 +255,20 @@ export interface LedgerRowProps {
|
|
|
230
255
|
export function LedgerRow(props: LedgerRowProps) {
|
|
231
256
|
const { label, meta, value, tone = "default", peek, peekWidth = 300, reference, accessibilityLabel } = props;
|
|
232
257
|
const rowDetails = useLoticsLocale().ledger.rowDetails;
|
|
233
|
-
const { format } = useLedger();
|
|
258
|
+
const { format, dropMeta } = useLedger();
|
|
259
|
+
// The caption goes entirely rather than shrinking to nothing. Yielding first
|
|
260
|
+
// (below) is the right ORDER but not a floor: measured at 375, a 30-character
|
|
261
|
+
// caption still wanted 184px of a ~250px text budget and clipped — and took
|
|
262
|
+
// the label down with it, ellipsising a 74px identity at 71px. Two separate
|
|
263
|
+
// authors had already worked around this by dropping `meta` at small widths
|
|
264
|
+
// in their own apps, which is the component's job.
|
|
265
|
+
const showMeta = meta != null && meta !== "" && !dropMeta;
|
|
234
266
|
const content = (
|
|
235
267
|
<>
|
|
236
268
|
<Text size="sm" numberOfLines={1} style={styles.shrink}>
|
|
237
269
|
{label}
|
|
238
270
|
</Text>
|
|
239
|
-
{
|
|
271
|
+
{showMeta ? (
|
|
240
272
|
<Text size="xs" color="muted" numberOfLines={1} style={styles.yield}>
|
|
241
273
|
{meta}
|
|
242
274
|
</Text>
|
package/src/matrix.tsx
CHANGED
|
@@ -143,12 +143,12 @@ function MatrixHeader({ corner, totalLabel }: MatrixHeaderProps) {
|
|
|
143
143
|
)}
|
|
144
144
|
</View>
|
|
145
145
|
{cols.map((c) => (
|
|
146
|
-
<Text key={c.key} size="
|
|
146
|
+
<Text key={c.key} size="sm" color="muted" weight="medium" align="right" numberOfLines={1} style={styles.colCell}>
|
|
147
147
|
{c.label}
|
|
148
148
|
</Text>
|
|
149
149
|
))}
|
|
150
150
|
{hasTotals ? (
|
|
151
|
-
<Text size="
|
|
151
|
+
<Text size="sm" color="muted" weight="medium" align="right" numberOfLines={1} style={[styles.totalCol, { width: totalColWidth }]}>
|
|
152
152
|
{resolvedTotalLabel}
|
|
153
153
|
</Text>
|
|
154
154
|
) : null}
|
|
@@ -325,12 +325,23 @@ const styles = StyleSheet.create({
|
|
|
325
325
|
height: 36,
|
|
326
326
|
borderRadius: 4,
|
|
327
327
|
borderWidth: 2,
|
|
328
|
-
|
|
328
|
+
// STRETCH, not center. The figure inside sets `align="right"`, and a Text
|
|
329
|
+
// centred by its parent SHRINK-WRAPS — so it is exactly as wide as its own
|
|
330
|
+
// glyphs and has nothing to align against. Right-aligning the text while
|
|
331
|
+
// this said `center` therefore changed the headers and totals (which carry
|
|
332
|
+
// no such centring) and left the cells where they were, so a figure sat up
|
|
333
|
+
// to 45px left of its own column header's right edge. `justifyContent`
|
|
334
|
+
// still centres vertically: in a column-direction box that is the main
|
|
335
|
+
// axis, and only `alignItems` decides the horizontal.
|
|
336
|
+
alignItems: "stretch",
|
|
329
337
|
justifyContent: "center",
|
|
330
338
|
},
|
|
331
339
|
colCell: {
|
|
332
340
|
flex: 1,
|
|
333
341
|
minWidth: 52,
|
|
342
|
+
// Matches the 2px the cell's own border insets its figure by, so a header
|
|
343
|
+
// and the numbers under it end on ONE x rather than two pixels apart.
|
|
344
|
+
paddingRight: 2,
|
|
334
345
|
},
|
|
335
346
|
totalCol: {
|
|
336
347
|
paddingLeft: 4,
|
package/src/option_list.tsx
CHANGED
|
@@ -157,7 +157,21 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
157
157
|
// description — is `MenuListItem`: the listbox row that GROWS. `MenuButton` is
|
|
158
158
|
// the fixed-height single-line menu row, and stays that for plain options.
|
|
159
159
|
return content || desc ? (
|
|
160
|
-
<MenuListItem
|
|
160
|
+
<MenuListItem
|
|
161
|
+
key={`${row.kind}-${opt.value}`}
|
|
162
|
+
{...shared}
|
|
163
|
+
title={content ?? label}
|
|
164
|
+
// A custom render and a description are INDEPENDENT facts about
|
|
165
|
+
// an option — how its value looks, and a subtitle under it — so
|
|
166
|
+
// one does not erase the other. Suppressing `desc` whenever
|
|
167
|
+
// `renderOptionContent` was set left a caller who wanted both
|
|
168
|
+
// with one slot for two things: they crammed the subtitle into
|
|
169
|
+
// the custom render, which `Select`/`InlineSelect` then reuse
|
|
170
|
+
// as the RESTING trigger display, so a gloss meant for the menu
|
|
171
|
+
// became a two-line control on every read. A caller who wants no
|
|
172
|
+
// subtitle passes no `getOptionDescription`.
|
|
173
|
+
description={desc}
|
|
174
|
+
/>
|
|
161
175
|
) : (
|
|
162
176
|
<MenuButton
|
|
163
177
|
key={`${row.kind}-${opt.value}`}
|