@lotics/ui 26.0.0 → 26.1.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 +8 -1
- package/package.json +1 -1
- package/src/reference_field.tsx +91 -67
package/docs/catalog.md
CHANGED
|
@@ -879,7 +879,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
879
879
|
derived value that rides along read-only); `multiline` for an address or an account block;
|
|
880
880
|
`type: "date"` swaps the draft's text input for a `DatePicker` and formats the read value —
|
|
881
881
|
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
|
|
882
|
+
free text and the format ambiguity never reaches the record; `action` hangs a verb ABOUT the
|
|
883
|
+
value on it (an `InlineButton` — "look this tax id up"), rendered AT REST only, because an
|
|
884
|
+
action writes the RECORD while a draft holds values seeded before it ran — fire one mid-draft
|
|
885
|
+
and Save writes the pre-action values back over what it just fetched.
|
|
886
|
+
The peek uses the popover's OWN anatomy: `PopoverHeader` for the identity, a scrolling body,
|
|
887
|
+
and a **pinned `PopoverFooter`** for the verbs. A long block therefore scrolls with its actions
|
|
888
|
+
still reachable — the earlier hand-rolled footer was a body child and scrolled away, hiding its
|
|
889
|
+
own Save.
|
|
883
890
|
`Edit` swaps the SAME grid's value cells for inputs — a DRAFT, so nothing commits until `Save`,
|
|
884
891
|
which fires `onSave` with **only the facts that CHANGED** (never a snapshot, so a lock or
|
|
885
892
|
`before_update` sees the real edit). This is what lets a peek hold editors at all: a
|
package/package.json
CHANGED
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,25 @@ 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
|
+
<PopoverContent style={{ width: 320, maxHeight: 420 }}>
|
|
278
|
+
<PopoverHeader>
|
|
262
279
|
<DialogSectionHeadingTitle description={code}>{name}</DialogSectionHeadingTitle>
|
|
280
|
+
</PopoverHeader>
|
|
281
|
+
<View style={{ gap: 12 }}>
|
|
263
282
|
{/* ONE geometry for both modes — the table's own 40px band, which is
|
|
264
283
|
`CONTROL_HEIGHT` and exactly what `TextInputField` renders at. The
|
|
265
284
|
read row therefore RESERVES the space its editor will need, and
|
|
@@ -305,15 +324,21 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
305
324
|
editors, border and all, and reaching for it means the
|
|
306
325
|
alignment survives the control geometry changing. Copying
|
|
307
326
|
the box here instead would drift the first time it does. */}
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
327
|
+
<View style={{ flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
328
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
329
|
+
<InlineStatic
|
|
330
|
+
/* A date's canonical value is its ISO string — that is
|
|
331
|
+
what the picker reads and what Save sends — so the
|
|
332
|
+
FORMATTING happens here, where the type is known.
|
|
333
|
+
Handing the caller that job would make `value` mean
|
|
334
|
+
two things (display in read, ISO in the draft) and the
|
|
335
|
+
two would drift. */
|
|
336
|
+
value={f.type === "date" ? formatDate(f.value, { locale: localeTag }) : f.value}
|
|
337
|
+
multiline={f.multiline}
|
|
338
|
+
/>
|
|
339
|
+
</View>
|
|
340
|
+
{f.action}
|
|
341
|
+
</View>
|
|
317
342
|
</DetailRow>
|
|
318
343
|
),
|
|
319
344
|
)}
|
|
@@ -339,15 +364,20 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
339
364
|
edit-only reference would otherwise draw a rule under the facts and
|
|
340
365
|
fence off an empty band, which is chrome asserting a structure that
|
|
341
366
|
is not there. */}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
367
|
+
</View>
|
|
368
|
+
{/* `PopoverFooter` — PINNED outside the scroller, and it owns the rule,
|
|
369
|
+
the full-bleed inset and the action-layout alignment this component
|
|
370
|
+
used to hand-roll. Hand-rolled, the verbs were a body child: they
|
|
371
|
+
scrolled away with the facts, so a long block hid its own Save.
|
|
372
|
+
|
|
373
|
+
THE DRAFT'S FOOTER REPLACES the peek's, it does not join it. Change
|
|
374
|
+
and Open are moves AWAY from an unsaved draft — one detaches the
|
|
375
|
+
record being edited, one navigates off it — so offering either here
|
|
376
|
+
would be offering to lose the typing. Cancel and Save are the only
|
|
377
|
+
two exits, which is also what the pinned popover promised. */}
|
|
378
|
+
<PopoverFooter align={editing ? "end" : "space-between"}>
|
|
348
379
|
{editing ? (
|
|
349
|
-
|
|
350
|
-
<View style={{ flex: 1 }} />
|
|
380
|
+
<>
|
|
351
381
|
<Button title={t.cancel} color="secondary" disabled={saving} onPress={closeDraft} />
|
|
352
382
|
{/* Disabled until something DIFFERS: with nothing to send, a save
|
|
353
383
|
is a write that fires the record's hooks and bumps its
|
|
@@ -358,52 +388,46 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
358
388
|
disabled={!dirty || saving}
|
|
359
389
|
onPress={() => void save()}
|
|
360
390
|
/>
|
|
361
|
-
|
|
391
|
+
</>
|
|
362
392
|
) : (
|
|
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>
|
|
393
|
+
<>
|
|
394
|
+
{/* LEFT acts on the LINK — which record this points at. Both are
|
|
395
|
+
unconditional: the peek always offers "point it elsewhere" and
|
|
396
|
+
"leave it empty", so its footer has ONE shape everywhere. */}
|
|
397
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
398
|
+
<Button
|
|
399
|
+
title={t.change}
|
|
400
|
+
color="secondary"
|
|
401
|
+
accessibilityLabel={`${t.change} — ${name}`}
|
|
402
|
+
onPress={() => { setPeekOpen(false); onChange(); }}
|
|
403
|
+
/>
|
|
404
|
+
{/* No fill — the least-reached verb here, and the one whose
|
|
405
|
+
result the reader is least likely to want by accident. */}
|
|
406
|
+
<Button
|
|
407
|
+
title={t.clear}
|
|
408
|
+
accessibilityLabel={`${t.clear} — ${name}`}
|
|
409
|
+
onPress={() => { setPeekOpen(false); onClear(); }}
|
|
410
|
+
/>
|
|
411
|
+
</View>
|
|
412
|
+
{/* RIGHT acts on the RECORD the link points at. Edit takes the ONE
|
|
413
|
+
filled-dark rung because it is the only verb here that leads to
|
|
414
|
+
a commit, and it hands that rung straight to Save when the
|
|
415
|
+
draft opens: one primary per mode, never two. */}
|
|
416
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
417
|
+
{editable ? (
|
|
418
|
+
<Button title={t.edit} color="primary" accessibilityLabel={`${t.edit} — ${name}`} onPress={openDraft} />
|
|
419
|
+
) : null}
|
|
420
|
+
{/* `openLabel` names the DESTINATION ("Open customer"): a page
|
|
421
|
+
carries four of these peeks, and four buttons announcing a
|
|
422
|
+
bare "Open" are four controls a screen reader cannot tell
|
|
423
|
+
apart. */}
|
|
424
|
+
{onOpen ? (
|
|
425
|
+
<Button title={t.open} color="secondary" accessibilityLabel={openLabel} onPress={() => { setPeekOpen(false); onOpen(); }} />
|
|
426
|
+
) : null}
|
|
427
|
+
</View>
|
|
428
|
+
</>
|
|
405
429
|
)}
|
|
406
|
-
</
|
|
430
|
+
</PopoverFooter>
|
|
407
431
|
</PopoverContent>
|
|
408
432
|
</Popover>
|
|
409
433
|
);
|