@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 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`. **`InlineMemberSelect` takes `avatarOnly`**
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "25.0.0",
3
+ "version": "25.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -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(false);
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 : []);
@@ -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
- <TextInputField
267
- value={draft[f.name] ?? ""}
268
- onChangeText={(v) => setDraft({ ...draft, [f.name as string]: v })}
269
- multiline={f.multiline}
270
- autoGrow={f.multiline}
271
- disabled={saving}
272
- accessibilityLabel={f.label}
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 value={f.value} multiline={f.multiline} />
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
  )}