@lotics/ui 26.0.0 → 26.4.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/docs/catalog.md +21 -4
- package/examples/tpl_record.tsx +2 -2
- package/package.json +1 -1
- package/src/detail_row.tsx +13 -2
- package/src/popover.tsx +42 -4
- package/src/reference_field.tsx +100 -68
package/docs/catalog.md
CHANGED
|
@@ -830,7 +830,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
830
830
|
grammar); raise `minValueWidth` (default 160) when a cell holds
|
|
831
831
|
MORE than one editor so the table stacks earlier. Two tables on one page share one grid by
|
|
832
832
|
repeating the same labelWidth. Worked example:
|
|
833
|
-
[`tpl_record`](../examples/tpl_record.tsx).
|
|
833
|
+
[`tpl_record`](../examples/tpl_record.tsx). A label's FIRST line centres on the control line, not the label block: a wrapped label grows DOWNWARD past the value's first line rather than dragging its own first line above it, so "Registered address" over two lines still starts level with the value it names.
|
|
834
834
|
- **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
|
|
835
835
|
row: `title` xxl semibold tabular, `subtitle` sm muted, `status` Badge slot, optional
|
|
836
836
|
`metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
|
|
@@ -854,7 +854,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
854
854
|
seam: **left, the LINK** — `Change` (`onChange`, `secondary`) points the field at a different
|
|
855
855
|
record, named for the intent and deliberately not `danger` since a recoverable correction must
|
|
856
856
|
not wear the colour reserved for destruction; `Clear` (`onClear`, NO fill — the least-reached
|
|
857
|
-
verb of the four) leaves it empty.
|
|
857
|
+
verb of the four) leaves it empty. The draft's footer follows the same rule: `Cancel` carries
|
|
858
|
+
no fill beside a filled `Save`, so the only filled button in either mode is the one that acts. **Both are REQUIRED**, so the footer has one shape
|
|
858
859
|
everywhere; a reference the record cannot do without gains nothing from hiding Clear, because
|
|
859
860
|
Change and then declining to pick reaches the same empty state. **Right, the RECORD it points at** — `Edit` (`primary`, the one filled-dark rung,
|
|
860
861
|
handed to `Save` when the draft opens) and `Open` (`secondary`, rightmost, OPTIONAL, the
|
|
@@ -879,7 +880,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
879
880
|
derived value that rides along read-only); `multiline` for an address or an account block;
|
|
880
881
|
`type: "date"` swaps the draft's text input for a `DatePicker` and formats the read value —
|
|
881
882
|
the fact's `value` stays the canonical ISO string on both sides, so a date is never edited as
|
|
882
|
-
free text and the format ambiguity never reaches the record
|
|
883
|
+
free text and the format ambiguity never reaches the record; `action` hangs a verb ABOUT the
|
|
884
|
+
value on it (an `InlineButton` — "look this tax id up"), rendered AT REST only, because an
|
|
885
|
+
action writes the RECORD while a draft holds values seeded before it ran — fire one mid-draft
|
|
886
|
+
and Save writes the pre-action values back over what it just fetched.
|
|
887
|
+
The peek uses the popover's OWN anatomy: `PopoverHeader` for the identity, a scrolling body,
|
|
888
|
+
and a **pinned `PopoverFooter`** for the verbs. A long block therefore scrolls with its actions
|
|
889
|
+
still reachable — the earlier hand-rolled footer was a body child and scrolled away, hiding its
|
|
890
|
+
own Save.
|
|
883
891
|
`Edit` swaps the SAME grid's value cells for inputs — a DRAFT, so nothing commits until `Save`,
|
|
884
892
|
which fires `onSave` with **only the facts that CHANGED** (never a snapshot, so a lock or
|
|
885
893
|
`before_update` sees the real edit). This is what lets a peek hold editors at all: a
|
|
@@ -1078,7 +1086,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1078
1086
|
never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes. Only the
|
|
1079
1087
|
`small` (bottom-sheet) presentation is modal (scrim). `PopoverContent` already insets its
|
|
1080
1088
|
body 12px — put content directly in it, NEVER add your own padding `View` (that double-pads);
|
|
1081
|
-
title/actions go in `PopoverHeader` / `PopoverFooter`.
|
|
1089
|
+
title/actions go in `PopoverHeader` / `PopoverFooter`. **A fixed width is the `width` PROP,
|
|
1090
|
+
never `style`**: `style` reaches only the BODY, and the panel is `max-content`, so a width set
|
|
1091
|
+
there governs the scroller while the header and footer sit outside it — the first long title
|
|
1092
|
+
then stretches the panel and leaves the content adrift in a box wider than itself. A popover
|
|
1093
|
+
with no bands never sees this, which is why `width` had to exist before one had both.
|
|
1094
|
+
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1095
|
+
amount, so a scrolling body reaches the panel's real edges — its content slides under the
|
|
1096
|
+
panel edge rather than under an invisible margin 12px inside it — while the text stays on the
|
|
1097
|
+
same column the header and footer use. Nothing to opt into; `disableBodyScroll` consumers
|
|
1098
|
+
(which own their scroll) are untouched.
|
|
1082
1099
|
- **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
|
|
1083
1100
|
built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes,
|
|
1084
1101
|
`goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -738,7 +738,7 @@ function PartyRow({ role, rec, options, placeholder, onPick, onOpen, onUnset, on
|
|
|
738
738
|
{ label: "Contact", value: rec.contact, name: "contact" },
|
|
739
739
|
{ label: "City", value: rec.city, name: "city" },
|
|
740
740
|
{ label: "Tax ID", value: rec.taxId, name: "taxId" },
|
|
741
|
-
{ label: "
|
|
741
|
+
{ label: "Registered address", value: rec.address, name: "address", multiline: true },
|
|
742
742
|
{ label: "Customer since", value: rec.since, name: "since", type: "date" },
|
|
743
743
|
]}
|
|
744
744
|
accessibilityLabel={`${rec.name} — details`}
|
|
@@ -1719,7 +1719,7 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
1719
1719
|
{ label: "Tax ID", value: customer.taxId, name: "taxId" },
|
|
1720
1720
|
{ label: "Contact", value: customer.contact, name: "contact" },
|
|
1721
1721
|
{ label: "City", value: customer.city, name: "city" },
|
|
1722
|
-
{ label: "
|
|
1722
|
+
{ label: "Registered address", value: customer.address, name: "address", multiline: true },
|
|
1723
1723
|
{ label: "Customer since", value: customer.since, name: "since", type: "date" },
|
|
1724
1724
|
]}
|
|
1725
1725
|
accessibilityLabel={`${customer.name} — details`}
|
package/package.json
CHANGED
package/src/detail_row.tsx
CHANGED
|
@@ -136,8 +136,17 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
136
136
|
// so the perceived gap matches a chip row's. Horizontal mode only (stacked
|
|
137
137
|
// mode has no band; it mirrors FormField's order).
|
|
138
138
|
const flatTuck = flat && annotated ? -Math.max(0, Math.round((minHeight - getInputLineHeight(false)) / 2)) : 0;
|
|
139
|
+
// A label CENTERS in the control line — but only its FIRST line does. Centring
|
|
140
|
+
// the whole block is identical while the label fits on one line and wrong the
|
|
141
|
+
// moment it wraps: a two-line label fills the band, so its first line starts at
|
|
142
|
+
// the band's top while the value's still sits at the control's own text inset,
|
|
143
|
+
// and the pair reads nine pixels out of step. Pad down by what centring a
|
|
144
|
+
// single line would have cost, then top-align: one line lands exactly where it
|
|
145
|
+
// always did, and every extra line grows downward past the value's first line
|
|
146
|
+
// instead of dragging it upward.
|
|
147
|
+
const labelLead = Math.max(0, Math.round((minHeight - getInputLineHeight(false)) / 2));
|
|
139
148
|
const labelCell = (cellStyle: StyleProp<ViewStyle>) => (
|
|
140
|
-
<View style={[styles.labelCell, { minHeight }, cellStyle]}>
|
|
149
|
+
<View style={[styles.labelCell, { minHeight, paddingTop: labelLead }, cellStyle]}>
|
|
141
150
|
{/* The label WRAPS inside its column — it is NEVER clipped. A field name
|
|
142
151
|
the reader can't finish is worse than a taller row, and a fixed
|
|
143
152
|
`labelWidth` guarantees the clip on every long name ("Registered
|
|
@@ -237,7 +246,9 @@ const styles = StyleSheet.create({
|
|
|
237
246
|
// tall as the band or taller, so it fills it and its FIRST line tops out
|
|
238
247
|
// level with the value's first control line. Do not "fix" this to
|
|
239
248
|
// `flex-start` — that drops every single-line label to the band's top edge.
|
|
240
|
-
|
|
249
|
+
// Top-aligned, with `labelLead` doing the single-line centring — see the
|
|
250
|
+
// note at its definition for why centring the BLOCK breaks a wrapped label.
|
|
251
|
+
labelCell: { justifyContent: "flex-start" },
|
|
241
252
|
// The first line of the value cell — the control centers in it exactly like
|
|
242
253
|
// the label cell does.
|
|
243
254
|
controlLine: { justifyContent: "center" },
|
package/src/popover.tsx
CHANGED
|
@@ -51,6 +51,16 @@ function usePopoverContext() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Root Popover component
|
|
54
|
+
/**
|
|
55
|
+
* The panel's own inset. The footer bleeds out by it to span its divider full
|
|
56
|
+
* width, and the scroll body does the same to reach the panel's real edges —
|
|
57
|
+
* three places that must agree, so they read it from here rather than each
|
|
58
|
+
* spelling `12`.
|
|
59
|
+
*/
|
|
60
|
+
const PANEL_INSET = 12;
|
|
61
|
+
const SCROLL_BODY = { marginHorizontal: -PANEL_INSET } as const;
|
|
62
|
+
const SCROLL_BODY_CONTENT = { paddingHorizontal: PANEL_INSET } as const;
|
|
63
|
+
|
|
54
64
|
export interface PopoverProps {
|
|
55
65
|
open?: boolean;
|
|
56
66
|
onOpenChange?: (open: boolean) => void;
|
|
@@ -187,6 +197,17 @@ export interface PopoverContentProps {
|
|
|
187
197
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
188
198
|
/** When true, body renders in a plain View instead of ScrollView (use when children manage their own scroll) */
|
|
189
199
|
disableBodyScroll?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* A fixed panel width, in px, covering ALL THREE BANDS.
|
|
202
|
+
*
|
|
203
|
+
* `style` reaches only the BODY, so a width set there governs the scroller and
|
|
204
|
+
* leaves `PopoverHeader`/`PopoverFooter` outside it — and the panel is
|
|
205
|
+
* `max-content`, so the first long title stretches the panel while the body
|
|
206
|
+
* stays put, leaving the content adrift in a box wider than itself. A popover
|
|
207
|
+
* with no bands never sees this, which is why it went unnoticed until one had
|
|
208
|
+
* both.
|
|
209
|
+
*/
|
|
210
|
+
width?: number;
|
|
190
211
|
/** When true, renders as bottom sheet on small screens (close button, slide-up animation) */
|
|
191
212
|
small?: boolean;
|
|
192
213
|
/** Accessible name for the bottom-sheet close button. Defaults to the locale's `overlay.close` ("Close" / "Đóng"). */
|
|
@@ -207,6 +228,7 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
207
228
|
style,
|
|
208
229
|
contentContainerStyle,
|
|
209
230
|
disableBodyScroll,
|
|
231
|
+
width,
|
|
210
232
|
small = false,
|
|
211
233
|
closeLabel = locale.overlay.close,
|
|
212
234
|
manageFocus = true,
|
|
@@ -602,7 +624,7 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
602
624
|
tabIndex={small ? undefined : -1}
|
|
603
625
|
style={{
|
|
604
626
|
position: "fixed",
|
|
605
|
-
padding:
|
|
627
|
+
padding: PANEL_INSET,
|
|
606
628
|
borderTopLeftRadius: 16,
|
|
607
629
|
borderTopRightRadius: 16,
|
|
608
630
|
borderBottomLeftRadius: small ? 0 : 16,
|
|
@@ -664,6 +686,8 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
664
686
|
width: Math.max(triggerWidth, MIN_CONTROL_WIDTH),
|
|
665
687
|
maxWidth: Math.max(triggerWidth, MIN_CONTROL_WIDTH),
|
|
666
688
|
}),
|
|
689
|
+
// Last, so it beats the `max-content` every branch above sets.
|
|
690
|
+
...(width != null && !small ? { width, maxWidth: width } : null),
|
|
667
691
|
}}
|
|
668
692
|
onClick={(e) => e.stopPropagation()}
|
|
669
693
|
// React synthetic events bubble through portals via the REACT tree, so a
|
|
@@ -692,7 +716,21 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
692
716
|
{disableBodyScroll ? (
|
|
693
717
|
<View style={style}>{bodyChildren}</View>
|
|
694
718
|
) : (
|
|
695
|
-
|
|
719
|
+
// FULL-BLEED HORIZONTALLY, then re-inset by the same 12. The panel
|
|
720
|
+
// pads all four sides, so a scroller sitting inside that padding
|
|
721
|
+
// clips its content 12px short of the edge and parks the scrollbar
|
|
722
|
+
// there too — content slides under an invisible margin instead of
|
|
723
|
+
// under the panel's own edge. Pulling out and padding back keeps the
|
|
724
|
+
// text on the exact column the header and footer use while giving the
|
|
725
|
+
// scroll its real edges. `PopoverFooter` already does this dance for
|
|
726
|
+
// its divider; the body needed it for the same reason.
|
|
727
|
+
//
|
|
728
|
+
// Horizontal only: the vertical padding is the gap to the header and
|
|
729
|
+
// footer, which is a gap the reader wants.
|
|
730
|
+
<ScrollView
|
|
731
|
+
style={[SCROLL_BODY, style]}
|
|
732
|
+
contentContainerStyle={[SCROLL_BODY_CONTENT, contentContainerStyle]}
|
|
733
|
+
>
|
|
696
734
|
{bodyChildren}
|
|
697
735
|
</ScrollView>
|
|
698
736
|
)}
|
|
@@ -719,9 +757,9 @@ export function PopoverFooter(props: PopoverFooterProps) {
|
|
|
719
757
|
return (
|
|
720
758
|
// Pull out to the popover's edges (counteract its 12px inset) so the divider
|
|
721
759
|
// spans full width; the action row then re-insets to align with the body.
|
|
722
|
-
<View style={{ marginHorizontal: -
|
|
760
|
+
<View style={{ marginHorizontal: -PANEL_INSET, marginTop: PANEL_INSET }}>
|
|
723
761
|
{showDivider && <Divider />}
|
|
724
|
-
<View style={{ paddingHorizontal:
|
|
762
|
+
<View style={{ paddingHorizontal: PANEL_INSET, paddingTop: PANEL_INSET, flexDirection: "row", alignItems: "center", gap: 8, justifyContent }}>
|
|
725
763
|
{children}
|
|
726
764
|
</View>
|
|
727
765
|
</View>
|
package/src/reference_field.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
1
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { DatePicker } from "./date_picker";
|
|
5
5
|
import { DetailRow, DetailTable } from "./detail_row";
|
|
6
6
|
import { formatDate } from "./format_date";
|
|
7
|
-
import { Divider } from "./divider";
|
|
8
7
|
import { InlineEditView } from "./inline_edit";
|
|
9
8
|
import { InlineStatic } from "./inline_static";
|
|
10
|
-
import { Popover, PopoverContent } from "./popover";
|
|
9
|
+
import { Popover, PopoverContent, PopoverFooter, PopoverHeader } from "./popover";
|
|
11
10
|
import { DialogSectionHeadingTitle } from "./section_heading";
|
|
12
11
|
import { Text } from "./text";
|
|
13
12
|
import { TextInputField } from "./text_input_field";
|
|
@@ -86,6 +85,17 @@ export interface ReferenceFact {
|
|
|
86
85
|
* the input.
|
|
87
86
|
*/
|
|
88
87
|
type?: "text" | "date";
|
|
88
|
+
/**
|
|
89
|
+
* A verb ABOUT this value — an `InlineButton`, per the kit's rule that such a
|
|
90
|
+
* verb travels with what it acts on. "Look this tax id up in the business
|
|
91
|
+
* register", "call this number".
|
|
92
|
+
*
|
|
93
|
+
* Rendered at REST, never inside the draft, and that is a correctness bound
|
|
94
|
+
* rather than a layout choice: an action here acts on the RECORD, while a
|
|
95
|
+
* draft holds values seeded before it ran. Fire one mid-draft and Save would
|
|
96
|
+
* write the pre-action values straight back over whatever it just fetched.
|
|
97
|
+
*/
|
|
98
|
+
action?: ReactNode;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
export interface ReferenceFieldProps {
|
|
@@ -250,16 +260,29 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
250
260
|
(`DialogSectionHeadingTitle`, ####) with the code as its description
|
|
251
261
|
rather than a hand-picked font weight; the facts are `DetailRow`s, which
|
|
252
262
|
is what label-beside-value IS everywhere else on this page; and the
|
|
253
|
-
|
|
263
|
+
verbs sit in the popover's own pinned footer rather than floating after
|
|
254
264
|
the last fact. Width matches `Peek`'s own content width so every peek in
|
|
255
265
|
an app is the same object.
|
|
256
266
|
`labelWidth` is the one override, and it is not arbitrary: a `DetailTable`
|
|
257
267
|
STACKS its columns below `labelWidth + MIN_CONTROL_WIDTH + 24`, so the
|
|
258
268
|
page's 150 would flip a 320 popover into stacked form grammar. 88 keeps
|
|
259
269
|
the summary side-by-side, which is the whole point of a glance. */}
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
{/* `PopoverContent` PARTITIONS its children: a `PopoverHeader` sits above
|
|
271
|
+
the scroller, a `PopoverFooter` is pinned below it, and everything else
|
|
272
|
+
scrolls between them. This component used none of that — it disabled
|
|
273
|
+
the body scroll and hand-rolled both bands as ordinary children, which
|
|
274
|
+
is invisible at three facts and fatal at eleven: the panel ran past the
|
|
275
|
+
viewport with no way to scroll, carrying its own Save button off-screen
|
|
276
|
+
with it. The three-fact fixture is what hid it. */}
|
|
277
|
+
{/* `width` on the PANEL, not `style` — `style` reaches only the body, so a
|
|
278
|
+
long referenced name in the header stretched the panel past it and the
|
|
279
|
+
facts sat in a box wider than themselves. `maxHeight` stays on the
|
|
280
|
+
body: that is the band that scrolls. */}
|
|
281
|
+
<PopoverContent width={320} style={{ maxHeight: 420 }}>
|
|
282
|
+
<PopoverHeader>
|
|
262
283
|
<DialogSectionHeadingTitle description={code}>{name}</DialogSectionHeadingTitle>
|
|
284
|
+
</PopoverHeader>
|
|
285
|
+
<View style={{ gap: 12 }}>
|
|
263
286
|
{/* ONE geometry for both modes — the table's own 40px band, which is
|
|
264
287
|
`CONTROL_HEIGHT` and exactly what `TextInputField` renders at. The
|
|
265
288
|
read row therefore RESERVES the space its editor will need, and
|
|
@@ -305,15 +328,21 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
305
328
|
editors, border and all, and reaching for it means the
|
|
306
329
|
alignment survives the control geometry changing. Copying
|
|
307
330
|
the box here instead would drift the first time it does. */}
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
331
|
+
<View style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
332
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
333
|
+
<InlineStatic
|
|
334
|
+
/* A date's canonical value is its ISO string — that is
|
|
335
|
+
what the picker reads and what Save sends — so the
|
|
336
|
+
FORMATTING happens here, where the type is known.
|
|
337
|
+
Handing the caller that job would make `value` mean
|
|
338
|
+
two things (display in read, ISO in the draft) and the
|
|
339
|
+
two would drift. */
|
|
340
|
+
value={f.type === "date" ? formatDate(f.value, { locale: localeTag }) : f.value}
|
|
341
|
+
multiline={f.multiline}
|
|
342
|
+
/>
|
|
343
|
+
</View>
|
|
344
|
+
{f.action}
|
|
345
|
+
</View>
|
|
317
346
|
</DetailRow>
|
|
318
347
|
),
|
|
319
348
|
)}
|
|
@@ -339,16 +368,25 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
339
368
|
edit-only reference would otherwise draw a rule under the facts and
|
|
340
369
|
fence off an empty band, which is chrome asserting a structure that
|
|
341
370
|
is not there. */}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
371
|
+
</View>
|
|
372
|
+
{/* `PopoverFooter` — PINNED outside the scroller, and it owns the rule,
|
|
373
|
+
the full-bleed inset and the action-layout alignment this component
|
|
374
|
+
used to hand-roll. Hand-rolled, the verbs were a body child: they
|
|
375
|
+
scrolled away with the facts, so a long block hid its own Save.
|
|
376
|
+
|
|
377
|
+
THE DRAFT'S FOOTER REPLACES the peek's, it does not join it. Change
|
|
378
|
+
and Open are moves AWAY from an unsaved draft — one detaches the
|
|
379
|
+
record being edited, one navigates off it — so offering either here
|
|
380
|
+
would be offering to lose the typing. Cancel and Save are the only
|
|
381
|
+
two exits, which is also what the pinned popover promised. */}
|
|
382
|
+
<PopoverFooter align={editing ? "end" : "space-between"}>
|
|
348
383
|
{editing ? (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
384
|
+
<>
|
|
385
|
+
{/* No fill, the same as `Clear` in the resting footer: in a pair of
|
|
386
|
+
one quiet exit and one commit, the exit carries no box. Save is
|
|
387
|
+
the only filled button in this mode, so nothing competes with
|
|
388
|
+
the act that writes. */}
|
|
389
|
+
<Button title={t.cancel} disabled={saving} onPress={closeDraft} />
|
|
352
390
|
{/* Disabled until something DIFFERS: with nothing to send, a save
|
|
353
391
|
is a write that fires the record's hooks and bumps its
|
|
354
392
|
modified stamp to record that nobody changed anything. */}
|
|
@@ -358,52 +396,46 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
358
396
|
disabled={!dirty || saving}
|
|
359
397
|
onPress={() => void save()}
|
|
360
398
|
/>
|
|
361
|
-
|
|
399
|
+
</>
|
|
362
400
|
) : (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
four of these peeks, and four buttons announcing a bare "Open"
|
|
400
|
-
are four controls a screen reader cannot tell apart. */}
|
|
401
|
-
{onOpen ? (
|
|
402
|
-
<Button title={t.open} color="secondary" accessibilityLabel={openLabel} onPress={() => { setPeekOpen(false); onOpen(); }} />
|
|
403
|
-
) : null}
|
|
404
|
-
</View>
|
|
401
|
+
<>
|
|
402
|
+
{/* LEFT acts on the LINK — which record this points at. Both are
|
|
403
|
+
unconditional: the peek always offers "point it elsewhere" and
|
|
404
|
+
"leave it empty", so its footer has ONE shape everywhere. */}
|
|
405
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
406
|
+
<Button
|
|
407
|
+
title={t.change}
|
|
408
|
+
color="secondary"
|
|
409
|
+
accessibilityLabel={`${t.change} — ${name}`}
|
|
410
|
+
onPress={() => { setPeekOpen(false); onChange(); }}
|
|
411
|
+
/>
|
|
412
|
+
{/* No fill — the least-reached verb here, and the one whose
|
|
413
|
+
result the reader is least likely to want by accident. */}
|
|
414
|
+
<Button
|
|
415
|
+
title={t.clear}
|
|
416
|
+
accessibilityLabel={`${t.clear} — ${name}`}
|
|
417
|
+
onPress={() => { setPeekOpen(false); onClear(); }}
|
|
418
|
+
/>
|
|
419
|
+
</View>
|
|
420
|
+
{/* RIGHT acts on the RECORD the link points at. Edit takes the ONE
|
|
421
|
+
filled-dark rung because it is the only verb here that leads to
|
|
422
|
+
a commit, and it hands that rung straight to Save when the
|
|
423
|
+
draft opens: one primary per mode, never two. */}
|
|
424
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
425
|
+
{editable ? (
|
|
426
|
+
<Button title={t.edit} color="primary" accessibilityLabel={`${t.edit} — ${name}`} onPress={openDraft} />
|
|
427
|
+
) : null}
|
|
428
|
+
{/* `openLabel` names the DESTINATION ("Open customer"): a page
|
|
429
|
+
carries four of these peeks, and four buttons announcing a
|
|
430
|
+
bare "Open" are four controls a screen reader cannot tell
|
|
431
|
+
apart. */}
|
|
432
|
+
{onOpen ? (
|
|
433
|
+
<Button title={t.open} color="secondary" accessibilityLabel={openLabel} onPress={() => { setPeekOpen(false); onOpen(); }} />
|
|
434
|
+
) : null}
|
|
435
|
+
</View>
|
|
436
|
+
</>
|
|
405
437
|
)}
|
|
406
|
-
</
|
|
438
|
+
</PopoverFooter>
|
|
407
439
|
</PopoverContent>
|
|
408
440
|
</Popover>
|
|
409
441
|
);
|