@lotics/ui 25.0.0 → 25.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 -2
- package/examples/tpl_record.tsx +12 -5
- package/package.json +1 -1
- package/src/inline_select.tsx +12 -2
- package/src/reference_field.tsx +42 -10
package/docs/catalog.md
CHANGED
|
@@ -720,7 +720,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
720
720
|
just text. **`InlineSelect` is single OR multi** — pass `multi` for a tag SET (`value: T[]`,
|
|
721
721
|
commits the new set on popover-CLOSE; selected tags render as badges via `renderSelected`),
|
|
722
722
|
mirroring `Select`'s `multi` axis; there is NO separate tag component. Both modes take
|
|
723
|
-
`allowCustom` (a create-a-tag/option row) + `searchable
|
|
723
|
+
`allowCustom` (a create-a-tag/option row) + `searchable`, plus **`autoFocus`** to open the list
|
|
724
|
+
on mount — for the picker a `ReferenceField`'s `Change` just dropped the reader into, so the
|
|
725
|
+
correction stays one gesture; leave it off for a field that is merely empty, since stealing the
|
|
726
|
+
list open on load is a different act. **`InlineMemberSelect` takes `avatarOnly`**
|
|
724
727
|
— the resting display is the bare AVATAR (no name/chevron, a dashed "add" ghost when unset) for a
|
|
725
728
|
DENSE row where the name won't fit (a task-row assignee); the dropdown rows still show avatar + name.
|
|
726
729
|
`InlineDatePicker` takes a **`tone`**
|
|
@@ -858,7 +861,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
858
861
|
`.save` / `.saving` / `.cancel`), while `openLabel` stays a prop because it names the
|
|
859
862
|
DESTINATION for a screen reader, which only the caller knows. **EDIT IN THE PEEK** (`onSave`):
|
|
860
863
|
a fact carrying a `name` is editable and that key is what it saves under (omit `name` for a
|
|
861
|
-
derived value that rides along read-only); `multiline` for an address or an account block
|
|
864
|
+
derived value that rides along read-only); `multiline` for an address or an account block;
|
|
865
|
+
`type: "date"` swaps the draft's text input for a `DatePicker` and formats the read value —
|
|
866
|
+
the fact's `value` stays the canonical ISO string on both sides, so a date is never edited as
|
|
867
|
+
free text and the format ambiguity never reaches the record.
|
|
862
868
|
`Edit` swaps the SAME grid's value cells for inputs — a DRAFT, so nothing commits until `Save`,
|
|
863
869
|
which fires `onSave` with **only the facts that CHANGED** (never a snapshot, so a lock or
|
|
864
870
|
`before_update` sees the real edit). This is what lets a peek hold editors at all: a
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -127,16 +127,19 @@ interface Customer {
|
|
|
127
127
|
/** Runs past one line at a peek's width — the case a fixture of short values
|
|
128
128
|
* never exercises, and the one where a read value and its editor diverge. */
|
|
129
129
|
address: string;
|
|
130
|
+
/** ISO. A date fact, so the peek's draft has one non-text editor to render —
|
|
131
|
+
* left as text it would take whatever shape the reader typed. */
|
|
132
|
+
since: string;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
// Atlas ships WITHOUT a tax ID — attach it to see the billing gate + the
|
|
133
136
|
// inline Tax ID fix-up in the Customer section.
|
|
134
137
|
const KNOWN_CUSTOMERS: Customer[] = [
|
|
135
|
-
{ id: "cus_01", name: "Northwind Traders", code: "KH-0148", taxId: "0312456780", contact: "Mara Lindqvist", city: "Gothenburg", address: "Ringvägen 118, 4 tr, 116 61 Stockholm, Sweden" },
|
|
136
|
-
{ id: "cus_02", name: "Harbor Freight Lines", code: "KH-0203", taxId: "0312998820", contact: "Diego Alvarez", city: "Rotterdam", address: "Waalhaven Oostzijde 81, 3087 BM Rotterdam, Netherlands" },
|
|
137
|
-
{ id: "cus_03", name: "Summit Packaging Co.", code: "KH-0231", taxId: "0301557742", contact: "Priya Nair", city: "Singapore", address: "9 Tuas Bay Walk, #03-14, Singapore 637803" },
|
|
138
|
-
{ id: "cus_04", name: "Atlas Distribution", code: "KH-0117", taxId: "", contact: "Tom Becker", city: "Hamburg", address: "Grosser Grasbrook 9, 20457 Hamburg, Germany" },
|
|
139
|
-
{ id: "cus_05", name: "Bluewater Logistics", code: "KH-0294", taxId: "0312004455", contact: "Lena Fischer", city: "Antwerp", address: "Noorderlaan 127, 2030 Antwerpen, Belgium" },
|
|
138
|
+
{ id: "cus_01", name: "Northwind Traders", code: "KH-0148", taxId: "0312456780", contact: "Mara Lindqvist", city: "Gothenburg", address: "Ringvägen 118, 4 tr, 116 61 Stockholm, Sweden", since: "2019-03-14" },
|
|
139
|
+
{ id: "cus_02", name: "Harbor Freight Lines", code: "KH-0203", taxId: "0312998820", contact: "Diego Alvarez", city: "Rotterdam", address: "Waalhaven Oostzijde 81, 3087 BM Rotterdam, Netherlands", since: "2021-11-02" },
|
|
140
|
+
{ id: "cus_03", name: "Summit Packaging Co.", code: "KH-0231", taxId: "0301557742", contact: "Priya Nair", city: "Singapore", address: "9 Tuas Bay Walk, #03-14, Singapore 637803", since: "2023-06-19" },
|
|
141
|
+
{ id: "cus_04", name: "Atlas Distribution", code: "KH-0117", taxId: "", contact: "Tom Becker", city: "Hamburg", address: "Grosser Grasbrook 9, 20457 Hamburg, Germany", since: "2018-01-30" },
|
|
142
|
+
{ id: "cus_05", name: "Bluewater Logistics", code: "KH-0294", taxId: "0312004455", contact: "Lena Fischer", city: "Antwerp", address: "Noorderlaan 127, 2030 Antwerpen, Belgium", since: "2022-09-08" },
|
|
140
143
|
];
|
|
141
144
|
|
|
142
145
|
const TAX_ID_RE = /^\d{10}(\d{3})?$/;
|
|
@@ -736,6 +739,7 @@ function PartyRow({ role, rec, options, placeholder, onPick, onOpen, onUnset, on
|
|
|
736
739
|
{ label: "City", value: rec.city, name: "city" },
|
|
737
740
|
{ label: "Tax ID", value: rec.taxId, name: "taxId" },
|
|
738
741
|
{ label: "Address", value: rec.address, name: "address", multiline: true },
|
|
742
|
+
{ label: "Customer since", value: rec.since, name: "since", type: "date" },
|
|
739
743
|
]}
|
|
740
744
|
accessibilityLabel={`${rec.name} — details`}
|
|
741
745
|
openLabel={`Open ${rec.name}`}
|
|
@@ -899,6 +903,7 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
899
903
|
city: patch.city ?? c.city,
|
|
900
904
|
taxId: patch.taxId ?? c.taxId,
|
|
901
905
|
address: patch.address ?? c.address,
|
|
906
|
+
since: patch.since ?? c.since,
|
|
902
907
|
}
|
|
903
908
|
: c,
|
|
904
909
|
),
|
|
@@ -1241,6 +1246,7 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
1241
1246
|
// Not collected at create — a new party is attached from three fields and
|
|
1242
1247
|
// the rest is filled in later, which is what the peek's Edit is for.
|
|
1243
1248
|
address: "",
|
|
1249
|
+
since: "",
|
|
1244
1250
|
};
|
|
1245
1251
|
setCustomers((prev) => [...prev, c]);
|
|
1246
1252
|
setCustomerId(c.id);
|
|
@@ -1714,6 +1720,7 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
|
|
|
1714
1720
|
{ label: "Contact", value: customer.contact, name: "contact" },
|
|
1715
1721
|
{ label: "City", value: customer.city, name: "city" },
|
|
1716
1722
|
{ label: "Address", value: customer.address, name: "address", multiline: true },
|
|
1723
|
+
{ label: "Customer since", value: customer.since, name: "since", type: "date" },
|
|
1717
1724
|
]}
|
|
1718
1725
|
accessibilityLabel={`${customer.name} — details`}
|
|
1719
1726
|
/* NO `onOpen`. The peek carries every fact this record
|
package/package.json
CHANGED
package/src/inline_select.tsx
CHANGED
|
@@ -13,6 +13,16 @@ import { useLoticsLocale } from "./locale";
|
|
|
13
13
|
|
|
14
14
|
interface InlineSelectBaseProps<T extends string, D = unknown> {
|
|
15
15
|
options: PickerOption<T, D>[];
|
|
16
|
+
/**
|
|
17
|
+
* Open the list on mount.
|
|
18
|
+
*
|
|
19
|
+
* For a picker that REPLACED something the reader just dismissed — a
|
|
20
|
+
* `ReferenceField` whose `onChange` fired, which means "wrong one, I'm about
|
|
21
|
+
* to pick another". Landing them on a closed picker charges a second press to
|
|
22
|
+
* resume a correction they already started. Leave it off for a field that is
|
|
23
|
+
* merely empty: stealing the list open on load is not the same act.
|
|
24
|
+
*/
|
|
25
|
+
autoFocus?: boolean;
|
|
16
26
|
/** Custom option content in the dropdown (icon + label, two-line, a badge…).
|
|
17
27
|
* Omit for a plain label list — both render through the same `OptionList`. */
|
|
18
28
|
renderOptionContent?: (option: PickerOption<T, D>) => ReactNode;
|
|
@@ -119,9 +129,9 @@ function InlineSelectShell(props: {
|
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
export function InlineSelect<T extends string, D = unknown>(props: InlineSelectProps<T, D>) {
|
|
122
|
-
const { options, renderOptionContent, renderSelected, placeholder, disabled, accessibilityLabel, searchable = false, allowCustom = false, customOptionLabel, variant, actions } = props;
|
|
132
|
+
const { options, renderOptionContent, renderSelected, placeholder, disabled, accessibilityLabel, searchable = false, allowCustom = false, customOptionLabel, variant, actions, autoFocus = false } = props;
|
|
123
133
|
const labels = useLoticsLocale().inline;
|
|
124
|
-
const [open, setOpen] = useState(
|
|
134
|
+
const [open, setOpen] = useState(autoFocus);
|
|
125
135
|
const [saving, setSaving] = useState(false);
|
|
126
136
|
const [error, setError] = useState<string | null>(null);
|
|
127
137
|
const [draft, setDraft] = useState<T[]>(props.multi ? props.value : []);
|
package/src/reference_field.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Button } from "./button";
|
|
4
|
+
import { DatePicker } from "./date_picker";
|
|
4
5
|
import { DetailRow, DetailTable } from "./detail_row";
|
|
6
|
+
import { formatDate } from "./format_date";
|
|
5
7
|
import { Divider } from "./divider";
|
|
6
8
|
import { InlineEditView } from "./inline_edit";
|
|
7
9
|
import { InlineStatic } from "./inline_static";
|
|
@@ -10,7 +12,7 @@ import { DialogSectionHeadingTitle } from "./section_heading";
|
|
|
10
12
|
import { Text } from "./text";
|
|
11
13
|
import { TextInputField } from "./text_input_field";
|
|
12
14
|
import { TextLink } from "./text_link";
|
|
13
|
-
import { useLoticsLocale } from "./locale";
|
|
15
|
+
import { useLocaleTag, useLoticsLocale } from "./locale";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* A REFERENCE to another record, rendered as a FIELD VALUE.
|
|
@@ -73,6 +75,17 @@ export interface ReferenceFact {
|
|
|
73
75
|
name?: string;
|
|
74
76
|
/** A value that runs past one line — an address, a bank account block. */
|
|
75
77
|
multiline?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* What the draft edits this fact WITH. `text` (default) is a text input;
|
|
80
|
+
* `date` is the kit's `DatePicker`, and the value is its ISO string.
|
|
81
|
+
*
|
|
82
|
+
* A date is not a short string that happens to look like one. Left as text it
|
|
83
|
+
* takes any shape the reader types, and the field it saves into accepts one —
|
|
84
|
+
* so the format ambiguity is only discovered by whatever reads the record
|
|
85
|
+
* next, which is exactly the kind of wrong a draft is supposed to catch at
|
|
86
|
+
* the input.
|
|
87
|
+
*/
|
|
88
|
+
type?: "text" | "date";
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export interface ReferenceFieldProps {
|
|
@@ -130,6 +143,9 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
130
143
|
// The peek's verbs are the component's OWN chrome, so they come from the
|
|
131
144
|
// pack — hardcoding them shipped "Open"/"Remove" into every localized app.
|
|
132
145
|
const t = useLoticsLocale().referenceField;
|
|
146
|
+
// The tag a date control renders in — `vi` yields dd/MM/yyyy with no per-
|
|
147
|
+
// instance prop, which is the same resolution `DatePicker` does internally.
|
|
148
|
+
const localeTag = useLocaleTag();
|
|
133
149
|
const anchor = useRef<View>(null);
|
|
134
150
|
const [peekOpen, setPeekOpen] = useState(false);
|
|
135
151
|
const [draft, setDraft] = useState<Record<string, string> | null>(null);
|
|
@@ -263,14 +279,22 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
263
279
|
{facts.map((f) =>
|
|
264
280
|
editing && f.name != null ? (
|
|
265
281
|
<DetailRow key={f.label} label={f.label}>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
282
|
+
{f.type === "date" ? (
|
|
283
|
+
<DatePicker
|
|
284
|
+
value={draft[f.name] || null}
|
|
285
|
+
onValueChange={(v) => setDraft({ ...draft, [f.name as string]: v })}
|
|
286
|
+
disabled={saving}
|
|
287
|
+
/>
|
|
288
|
+
) : (
|
|
289
|
+
<TextInputField
|
|
290
|
+
value={draft[f.name] ?? ""}
|
|
291
|
+
onChangeText={(v) => setDraft({ ...draft, [f.name as string]: v })}
|
|
292
|
+
multiline={f.multiline}
|
|
293
|
+
autoGrow={f.multiline}
|
|
294
|
+
disabled={saving}
|
|
295
|
+
accessibilityLabel={f.label}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
274
298
|
</DetailRow>
|
|
275
299
|
) : (
|
|
276
300
|
<DetailRow key={f.label} label={f.label}>
|
|
@@ -281,7 +305,15 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
281
305
|
editors, border and all, and reaching for it means the
|
|
282
306
|
alignment survives the control geometry changing. Copying
|
|
283
307
|
the box here instead would drift the first time it does. */}
|
|
284
|
-
<InlineStatic
|
|
308
|
+
<InlineStatic
|
|
309
|
+
/* A date's canonical value is its ISO string — that is what
|
|
310
|
+
the picker reads and what Save sends — so the FORMATTING
|
|
311
|
+
happens here, where the type is known. Handing the caller
|
|
312
|
+
that job would make `value` mean two things (display in
|
|
313
|
+
read, ISO in the draft) and the two would drift. */
|
|
314
|
+
value={f.type === "date" ? formatDate(f.value, { locale: localeTag }) : f.value}
|
|
315
|
+
multiline={f.multiline}
|
|
316
|
+
/>
|
|
285
317
|
</DetailRow>
|
|
286
318
|
),
|
|
287
319
|
)}
|