@lotics/ui 27.13.0 → 27.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/composition.md +8 -6
- package/docs/data_entry.md +31 -0
- package/docs/templates.md +23 -2
- package/examples/tpl_record.tsx +78 -65
- package/package.json +1 -1
- package/src/detail_row.tsx +21 -2
package/docs/composition.md
CHANGED
|
@@ -494,7 +494,7 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
494
494
|
(colored `Text`), not the buttons.
|
|
495
495
|
- **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee". A create is `primary` and
|
|
496
496
|
rides its section's HEADING row at the right edge — see § The add-placement law for why, and
|
|
497
|
-
for the one-verb-one-weight rule that keeps it primary
|
|
497
|
+
for the one-verb-one-weight rule that keeps it `primary` wherever it renders. (The
|
|
498
498
|
icon-only `+` `IconButton` — a composer's attach — is a different thing and keeps its glyph.)
|
|
499
499
|
|
|
500
500
|
## The action-gating law — disabled + the reason as a co-located Callout
|
|
@@ -600,11 +600,13 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
600
600
|
|
|
601
601
|
The ADD is the one verb this law does NOT place next to its effect, and the add-placement law
|
|
602
602
|
says why: a position that never moves beats a position that is adjacent, because "below the
|
|
603
|
-
last row" is a different place on every list and no place at all on an empty one.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
603
|
+
last row" is a different place on every list and no place at all on an empty one. The heading
|
|
604
|
+
row therefore carries identity + meta + view controls + the section's ADD, and no other content
|
|
605
|
+
verb. **An add is `primary` wherever it renders** — on that heading row, or inside an
|
|
606
|
+
`EmptyState` in the one case § Empty states allows, where there is no heading to carry it. It
|
|
607
|
+
is the act its section offers, so it carries that section's weight; a lone `secondary` button
|
|
608
|
+
reads as though the real action were somewhere else. The rule that matters is that ONE VERB
|
|
609
|
+
KEEPS ONE WEIGHT — the same add must not be primary on an empty list and secondary on a full one.
|
|
608
610
|
A record surface therefore carries several primaries, one per section, and that is correct:
|
|
609
611
|
"one primary per SCREEN" governs the page BAND, not a stack of independent sections each with
|
|
610
612
|
its own single act. The cost is real and worth knowing — the more sections offer an act, the
|
package/docs/data_entry.md
CHANGED
|
@@ -13,6 +13,7 @@ in [the templates](./templates.md) (`examples/tpl_*.tsx`).
|
|
|
13
13
|
| You're capturing… | Reach for | Why |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| an EXISTING record's fields | [**Inline edit**](#inline-edit--the-preferred-way-to-edit-an-existing-record) (`Inline*`) | edit in place, no form mode |
|
|
16
|
+
| a DRAFT inside a `Dialog` (create / edit-then-save) | [**`FormField` / `FormTextInput`**](#a-dialog-is-a-draft--use-formfield-never-an-inline-editor), controlled | the dialog's action IS the commit; inline editors make everything derived from the draft lag by one blur |
|
|
16
17
|
| a value in a DENSE ROW / register `DataGrid` cell (a task's due, assignee, status) | the SAME `Inline*` editor in **`variant="bare"`** | one editor per value type; `bare` drops the resting frame (the grid already promises every cell edits) and fades the same border in on hover — there is no separate `*Cell` family |
|
|
17
18
|
| a brand-NEW record | **create-then-refine** (`tpl_record`: "New" is ONE CLICK → a fresh Draft; everything refines in place on the record surface) | nobody fills 5 sections in one sitting; the surface is the editor |
|
|
18
19
|
| a RELATED record (pick or make) | [**find-or-create**](#find-or-create--the-combobox-family-is-the-control) (`Combobox allowCustom`) | one control covers both |
|
|
@@ -41,6 +42,36 @@ the CONTROL COLUMN, the same left edge the user just filled, and inherits stacke
|
|
|
41
42
|
narrow containers. A right-floated button aligns to nothing; only OVERLAY footers
|
|
42
43
|
(`DialogFooter`/`DrawerFooter`) right-align. Worked: `tpl_record`'s Delivery receipt.
|
|
43
44
|
|
|
45
|
+
### A dialog is a draft — use `FormField`, never an `Inline*` editor
|
|
46
|
+
|
|
47
|
+
An inline editor SELF-PERSISTS: it holds a local draft and reports it on blur (or Enter). A
|
|
48
|
+
`FormField` wraps a draft **validated and committed together**. A create/edit **`Dialog` is
|
|
49
|
+
definitionally the second** — its Save/Create is the commit — so build it from `FormField` /
|
|
50
|
+
`FormTextInput` with controlled `value` + `onChangeText`, even when the record surface behind it
|
|
51
|
+
is a grid of inline editors.
|
|
52
|
+
|
|
53
|
+
This is **not** a safety rule, and it does not soften the press-gate law below ("Commit-on-blur
|
|
54
|
+
would race the press that caused it"): inline editors in a dialog lose nothing, because `Button`
|
|
55
|
+
already holds its press until the blur's commit settles. The gate makes the press SAFE. What it cannot do is make the dialog HONEST
|
|
56
|
+
while the field still has focus — it only acts at press time, and everything **derived** from the
|
|
57
|
+
draft trails the typing until then: a validity `error`, a Save gated on that validity, a computed
|
|
58
|
+
total. Type a valid value and the form still says invalid, and its Save still sits disabled, until
|
|
59
|
+
focus leaves. That is the dialog arguing with the box the reader is looking at, and no amount of
|
|
60
|
+
press-gating answers it, because the reader is not pressing anything yet.
|
|
61
|
+
|
|
62
|
+
Controlled inputs make derived state track what is on screen, and read shorter than the editors
|
|
63
|
+
they replace — four `DetailRow` + `Inline*` pairs whose `onSave` exists only to `setState` collapse
|
|
64
|
+
to four fields. Worked example: `tpl_record`'s New-customer dialog.
|
|
65
|
+
|
|
66
|
+
**But do not then derive the ERROR per keystroke** — the mistake controlled inputs invite, and the
|
|
67
|
+
opposite failure to the one above. Validity and the error MESSAGE are different questions. Validity
|
|
68
|
+
gates the commit and is checked continuously; the message claims the reader did something wrong, so
|
|
69
|
+
it may only be made once they are DONE. Every prefix of a correct fixed-length value is invalid, so
|
|
70
|
+
a per-keystroke message turns the first digit of a right answer into a red complaint. Gate the
|
|
71
|
+
message on a `touched` flag set `onBlur`, keep the commit gated on the real validity, and let a
|
|
72
|
+
showing error clear live as the value becomes valid — the shape [`useForm`](../src/use_form.ts)
|
|
73
|
+
already has: errors arrive from a validate pass, and editing a field clears that field's error.
|
|
74
|
+
|
|
44
75
|
## Inline edit — the preferred way to edit an existing record
|
|
45
76
|
|
|
46
77
|
When the whole record is editable (a detail/record screen, dense settings), don't wrap it in a
|
package/docs/templates.md
CHANGED
|
@@ -53,6 +53,22 @@ the package index is [../AGENTS.md](../AGENTS.md).
|
|
|
53
53
|
| AI ranks answers — look-up-and-explain | `tpl_lookup` |
|
|
54
54
|
| AI over a record's documents (extract / cross-check / generate) | `tpl_record` — its Documents + Document set sections |
|
|
55
55
|
|
|
56
|
+
### Register + drawer, or a navigated page?
|
|
57
|
+
|
|
58
|
+
The register is the DEFAULT, and its row press opens the workspace `Drawer`. A record earns
|
|
59
|
+
its own navigated route only when one of these holds:
|
|
60
|
+
|
|
61
|
+
- **It is a destination.** Someone arrives cold from outside the register — a link in an
|
|
62
|
+
email, a notification, a handoff from another desk — and must land on the record without
|
|
63
|
+
the list to open it from.
|
|
64
|
+
- **The work needs the canvas.** A docked comments gutter, a document read beside the fields,
|
|
65
|
+
an outline rail worth navigating — width the drawer's single column cannot give.
|
|
66
|
+
|
|
67
|
+
The volume of DATA is not the test. A record with forty fields still closes in the drawer if
|
|
68
|
+
the operator opens it from the list, works it, and goes back to the list. What an app without
|
|
69
|
+
a destination skips is the ROUTE, never the template: the drawer renders `tpl_record` either
|
|
70
|
+
way, so "no record page" must never become a hand-built detail surface.
|
|
71
|
+
|
|
56
72
|
## Composition rules — how a record surface is assembled
|
|
57
73
|
|
|
58
74
|
Laws for any screen where a record's data is edited and a primary action produces its output
|
|
@@ -268,7 +284,10 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
268
284
|
ATTENTION state as a co-located `Callout` (e.g. overdue — record-scoped state reads here;
|
|
269
285
|
field-scoped state stays on its field), then a quiet `SummaryLine` (desk, documents
|
|
270
286
|
, to collect).
|
|
271
|
-
- **GENERAL — the MAIN details,
|
|
287
|
+
- **GENERAL — the MAIN details, first in THIS record's rail** because its work starts at the
|
|
288
|
+
details; rule 1 orders sections by work flow, so a surface whose work starts at a DOCUMENT
|
|
289
|
+
(an invoice desk, a claims desk) leads with Documents and General follows. Every later
|
|
290
|
+
section is supplementary.
|
|
272
291
|
A `SubsectionStack` (space-only beat) of a headingless LEAD group (the key facts) plus
|
|
273
292
|
named groups (e.g. Order / Fulfilment / System — system identifiers get a HOME, not a
|
|
274
293
|
per-row badge). Every group's `DetailTable` repeats the same `labelWidth` so the grid
|
|
@@ -323,7 +342,9 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
323
342
|
a floating `Callout`. Transport's three party rows are the same shape repeated, which is where
|
|
324
343
|
it earns itself: N references as field rows scan as one list, where N cards would be a wall.
|
|
325
344
|
Empty state = the find-or-create `Combobox` in the same value slot, custom row → the create
|
|
326
|
-
`Dialog` (tax ID + Fetch
|
|
345
|
+
`Dialog` (tax ID + Fetch). That dialog is built from **`FormField` / `FormTextInput`, never the
|
|
346
|
+
record surface's `Inline*` editors** — see [data_entry.md](./data_entry.md) §"A dialog is a
|
|
347
|
+
draft".
|
|
327
348
|
**Reference (the buyer's own) is the `actions` worked example**: its `Copy` is an
|
|
328
349
|
`InlineButton` ON the field, passed unconditionally and `disabled` while empty — not in the
|
|
329
350
|
row's `trailing`, and not conditional, which would resize the field as you type.
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -1036,7 +1036,9 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
1036
1036
|
// — and the draft names its destination as DATA instead of the dialog holding a
|
|
1037
1037
|
// callback, so what is in flight stays inspectable and the commit stays one
|
|
1038
1038
|
// function.
|
|
1039
|
-
|
|
1039
|
+
// `taxIdTouched` is what decides whether the tax ID's format error may SHOW —
|
|
1040
|
+
// see `taxIdError` for why a controlled field must not simply derive it.
|
|
1041
|
+
const [custDraft, setCustDraft] = useState<{ target: PartyTarget; name: string; taxId: string; contact: string; city: string; taxIdTouched: boolean } | null>(null);
|
|
1040
1042
|
// The Fetch button's in-flight state (the registry lookup).
|
|
1041
1043
|
const [fetching, setFetching] = useState(false);
|
|
1042
1044
|
|
|
@@ -1369,11 +1371,22 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
1369
1371
|
attachParty[target](opt.value);
|
|
1370
1372
|
return;
|
|
1371
1373
|
}
|
|
1372
|
-
setCustDraft({ target, name: opt.value, taxId: "", contact: "", city: "" });
|
|
1374
|
+
setCustDraft({ target, name: opt.value, taxId: "", contact: "", city: "", taxIdTouched: false });
|
|
1373
1375
|
};
|
|
1374
1376
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
+
// Two different questions, and conflating them is the trap a controlled field
|
|
1378
|
+
// walks into. VALIDITY gates the commit and is checked continuously. The
|
|
1379
|
+
// ERROR MESSAGE is a claim that the reader did something wrong, and it may
|
|
1380
|
+
// only be made once they are DONE — every prefix of a correct 10-digit id is
|
|
1381
|
+
// invalid, so deriving the message per keystroke turns "1" into a red
|
|
1382
|
+
// complaint and shouts at someone typing the right answer.
|
|
1383
|
+
//
|
|
1384
|
+
// Blur is "done", which is why `taxIdTouched` gates only the message. Once it
|
|
1385
|
+
// is showing, it clears the moment the value becomes valid — the same shape
|
|
1386
|
+
// `useForm` has: errors arrive from a validate pass, and editing a field
|
|
1387
|
+
// clears that field's error.
|
|
1388
|
+
const taxIdInvalid = !!custDraft && custDraft.taxId.trim() !== "" && !TAX_ID_RE.test(custDraft.taxId.trim());
|
|
1389
|
+
const taxIdError = taxIdInvalid && custDraft?.taxIdTouched ? "Tax ID is 10 or 13 digits" : undefined;
|
|
1377
1390
|
|
|
1378
1391
|
const createAndAttach = () => {
|
|
1379
1392
|
if (!custDraft) return;
|
|
@@ -3217,46 +3230,53 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
3217
3230
|
|
|
3218
3231
|
{/* find-or-create's CREATE branch — a focused Dialog (4 fields is
|
|
3219
3232
|
dialog-weight; an inline swap shifts the page). Essentials only: the
|
|
3220
|
-
dialog's Fetch fills contact + city off the tax ID.
|
|
3233
|
+
dialog's Fetch fills contact + city off the tax ID.
|
|
3234
|
+
|
|
3235
|
+
FORM controls, not the record surface's inline editors, and that is the
|
|
3236
|
+
whole point of the dialog: an inline editor SELF-PERSISTS on blur, a
|
|
3237
|
+
`FormField` wraps a draft validated and committed TOGETHER. This dialog
|
|
3238
|
+
is definitionally the second, so it is built from the second.
|
|
3239
|
+
|
|
3240
|
+
It was built from the first, and NOTHING WAS LOST BY THAT — the kit
|
|
3241
|
+
gates the press (`pending_commits`), so `Create & attach` waited for the
|
|
3242
|
+
blur's commit and a typed tax ID always survived. This is not a bug fix;
|
|
3243
|
+
the safety was never in question.
|
|
3244
|
+
|
|
3245
|
+
What the gate cannot do is make the dialog honest BEFORE the press. It
|
|
3246
|
+
acts at press time, so until then everything derived from the draft
|
|
3247
|
+
trails the typing: with a valid id on screen the field still read
|
|
3248
|
+
invalid and the button still sat disabled, until focus left. The reader
|
|
3249
|
+
is not pressing anything yet, so no press-gating answers it.
|
|
3250
|
+
|
|
3251
|
+
Controlled inputs make derived state track what is on screen — and cost
|
|
3252
|
+
less to read than the editors they replace. */}
|
|
3221
3253
|
<Dialog width={520} open={custDraft !== null} onOpenChange={(o) => { if (!o) setCustDraft(null); }}>
|
|
3222
3254
|
<DialogHeader>
|
|
3223
3255
|
<DialogHeaderTitle>New customer</DialogHeaderTitle>
|
|
3224
3256
|
</DialogHeader>
|
|
3225
|
-
{
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
{
|
|
3241
|
-
FormField's alert semantics), not a loose line below the table */}
|
|
3242
|
-
<DetailRow label="Tax ID" error={taxIdError}>
|
|
3243
|
-
{/* Fetch is NOT a verb about this value — it reads the tax ID and
|
|
3244
|
-
fills the contact and city BELOW it, so it belongs to the form,
|
|
3245
|
-
not to the field (which is why it isn't an `InlineButton` in
|
|
3246
|
-
`actions`). It composes into a row inside this value cell, so
|
|
3247
|
-
it costs the one row that wants it instead of a column every
|
|
3248
|
-
row would reserve. */}
|
|
3249
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }}>
|
|
3257
|
+
<View style={{ paddingHorizontal: 24, paddingBottom: 12, gap: 16 }}>
|
|
3258
|
+
<FormTextInput
|
|
3259
|
+
label="Customer name"
|
|
3260
|
+
value={custDraft?.name ?? ""}
|
|
3261
|
+
onChangeText={(v) => setCustDraft((d) => (d ? { ...d, name: v } : d))}
|
|
3262
|
+
placeholder="Add legal name…"
|
|
3263
|
+
/>
|
|
3264
|
+
{/* Fetch is NOT a verb about this value — it reads the tax ID and
|
|
3265
|
+
fills the contact and city BELOW it, so it belongs to the form, not
|
|
3266
|
+
to the field. It composes into a row inside this field, so it costs
|
|
3267
|
+
the one field that wants it instead of a column every field would
|
|
3268
|
+
reserve. `FormField` + a bare input (rather than `FormTextInput`)
|
|
3269
|
+
is what lets the button share the line while the label, error and
|
|
3270
|
+
a11y wiring still belong to the input. */}
|
|
3271
|
+
<FormField label="Tax ID" error={taxIdError}>
|
|
3272
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
3250
3273
|
<View style={{ flex: 1, minWidth: 0 }}>
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
setCustDraft((d) => (d ? { ...d,
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
placeholder="Add tax ID…"
|
|
3258
|
-
accessibilityLabel="Tax ID"
|
|
3259
|
-
/>
|
|
3274
|
+
<TextInputField
|
|
3275
|
+
value={custDraft?.taxId ?? ""}
|
|
3276
|
+
onChangeText={(v) => setCustDraft((d) => (d ? { ...d, taxId: v } : d))}
|
|
3277
|
+
onBlur={() => setCustDraft((d) => (d ? { ...d, taxIdTouched: true } : d))}
|
|
3278
|
+
placeholder="Add tax ID…"
|
|
3279
|
+
/>
|
|
3260
3280
|
</View>
|
|
3261
3281
|
<Button
|
|
3262
3282
|
title="Fetch"
|
|
@@ -3265,35 +3285,28 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
3265
3285
|
disabled={!TAX_ID_RE.test((custDraft?.taxId ?? "").trim())}
|
|
3266
3286
|
onPress={fetchIntoDraft}
|
|
3267
3287
|
/>
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
<InlineTextInput
|
|
3283
|
-
value={custDraft?.city ?? ""}
|
|
3284
|
-
onSave={(v) => {
|
|
3285
|
-
setCustDraft((d) => (d ? { ...d, city: v } : d));
|
|
3286
|
-
return Promise.resolve();
|
|
3287
|
-
}}
|
|
3288
|
-
placeholder="Add city…"
|
|
3289
|
-
accessibilityLabel="City"
|
|
3290
|
-
/>
|
|
3291
|
-
</DetailRow>
|
|
3292
|
-
</DetailTable>
|
|
3288
|
+
</View>
|
|
3289
|
+
</FormField>
|
|
3290
|
+
<FormTextInput
|
|
3291
|
+
label="Contact"
|
|
3292
|
+
value={custDraft?.contact ?? ""}
|
|
3293
|
+
onChangeText={(v) => setCustDraft((d) => (d ? { ...d, contact: v } : d))}
|
|
3294
|
+
placeholder="Add contact…"
|
|
3295
|
+
/>
|
|
3296
|
+
<FormTextInput
|
|
3297
|
+
label="City"
|
|
3298
|
+
value={custDraft?.city ?? ""}
|
|
3299
|
+
onChangeText={(v) => setCustDraft((d) => (d ? { ...d, city: v } : d))}
|
|
3300
|
+
placeholder="Add city…"
|
|
3301
|
+
/>
|
|
3293
3302
|
</View>
|
|
3294
3303
|
<DialogFooter>
|
|
3295
3304
|
<Button title="Cancel" color="secondary" onPress={() => setCustDraft(null)} />
|
|
3296
|
-
|
|
3305
|
+
{/* Gated on the VALIDITY, never on the visible message: an invalid id
|
|
3306
|
+
must not commit just because the reader has not blurred out of the
|
|
3307
|
+
field yet. Pressing here blurs first, so the explanation appears in
|
|
3308
|
+
the same gesture that finds the button disabled. */}
|
|
3309
|
+
<Button title="Create & attach" color="primary" disabled={(custDraft?.name.trim() ?? "") === "" || taxIdInvalid} onPress={createAndAttach} />
|
|
3297
3310
|
</DialogFooter>
|
|
3298
3311
|
</Dialog>
|
|
3299
3312
|
|
package/package.json
CHANGED
package/src/detail_row.tsx
CHANGED
|
@@ -204,7 +204,7 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
204
204
|
{flatTuck !== 0 ? <View style={{ marginTop: flatTuck }}>{annotations}</View> : annotations}
|
|
205
205
|
</View>
|
|
206
206
|
) : (
|
|
207
|
-
<View style={annotated
|
|
207
|
+
<View style={[styles.spreadValue, annotated && styles.valueStack]}>
|
|
208
208
|
<View style={[styles.controlLine, { minHeight }]}>{children}</View>
|
|
209
209
|
{flatTuck !== 0 ? <View style={{ marginTop: flatTuck }}>{annotations}</View> : annotations}
|
|
210
210
|
</View>
|
|
@@ -239,7 +239,20 @@ const styles = StyleSheet.create({
|
|
|
239
239
|
alignItems: "flex-start",
|
|
240
240
|
gap: 12,
|
|
241
241
|
},
|
|
242
|
-
|
|
242
|
+
// Spread mode's label column. `flexGrow` is what pushes the value to the right
|
|
243
|
+
// edge; the BASIS is what stops the push from eating the label. It was
|
|
244
|
+
// `flex: 1` — grow 1, shrink 1, basis **0** — so the label entered the layout
|
|
245
|
+
// claiming no size at all, and a value too wide for the row (a company name in
|
|
246
|
+
// a stacked register) took the whole row while the label absorbed the entire
|
|
247
|
+
// deficit: measured at 0px wide and 140px tall, "Hợp đồng" set one character
|
|
248
|
+
// per line down the page.
|
|
249
|
+
//
|
|
250
|
+
// With `basis: auto` the label enters at its TEXT width, so a deficit is split
|
|
251
|
+
// between the two in proportion to what each actually asked for — a long value
|
|
252
|
+
// is the one that gives way, because it is the one that is long. Both stay
|
|
253
|
+
// shrinkable on purpose: a long LABEL against a short value has to wrap too,
|
|
254
|
+
// and pinning the label unshrinkable would just overflow the row the other way.
|
|
255
|
+
flexLabel: { flexGrow: 1, flexShrink: 1, flexBasis: "auto" },
|
|
243
256
|
// `center` is what makes a WRAPPED label align sanely, and it does both jobs
|
|
244
257
|
// with no measurement: a one-line label is shorter than the control band, so
|
|
245
258
|
// it centers on the control line (the alignment law); a wrapped label is as
|
|
@@ -255,6 +268,12 @@ const styles = StyleSheet.create({
|
|
|
255
268
|
// The 2px between the control line and the annotation block beneath it — the
|
|
256
269
|
// same rhythm `FieldAnnotations` puts between its own lines.
|
|
257
270
|
valueStack: { gap: 2 },
|
|
271
|
+
// Spread mode's value. A React Native `View` does NOT shrink by default
|
|
272
|
+
// (`flexShrink: 0`, unlike the web), so without this a wide value simply keeps
|
|
273
|
+
// its intrinsic width — overflowing the row and pushing the whole deficit onto
|
|
274
|
+
// the label. `minWidth: 0` is the half that lets a text value actually wrap
|
|
275
|
+
// rather than stopping at its longest word.
|
|
276
|
+
spreadValue: { flexShrink: 1, minWidth: 0 },
|
|
258
277
|
// Form mode (labelWidth set): the value column FILLS the row so every inline
|
|
259
278
|
// editor spans the SAME width — and none jumps wider when it swaps to the
|
|
260
279
|
// (flex:1) edit control. A stretch column, so an InlineEditView / InlineStatic
|