@lotics/ui 7.19.3 → 9.0.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.
Files changed (75) hide show
  1. package/AGENTS.md +298 -159
  2. package/examples/tpl_allocate.tsx +2 -2
  3. package/examples/tpl_attendance.tsx +2 -2
  4. package/examples/tpl_calendar.tsx +1 -1
  5. package/examples/tpl_dashboard.tsx +1 -1
  6. package/examples/tpl_dieline.tsx +3 -3
  7. package/examples/tpl_documents.tsx +790 -0
  8. package/examples/tpl_item_list.tsx +1015 -124
  9. package/examples/tpl_lookup.tsx +37 -10
  10. package/examples/tpl_pick.tsx +3 -3
  11. package/examples/tpl_pivot.tsx +1 -1
  12. package/examples/tpl_record.tsx +1354 -0
  13. package/examples/tpl_report.tsx +7 -7
  14. package/examples/tpl_rollup.tsx +6 -6
  15. package/examples/tpl_shifts.tsx +2 -2
  16. package/examples/tpl_statements.tsx +221 -0
  17. package/examples/tpl_stock.tsx +7 -7
  18. package/examples/tpl_task_board.tsx +48 -32
  19. package/examples/tpl_tasks.tsx +47 -47
  20. package/examples/tpl_tower.tsx +2 -2
  21. package/package.json +9 -9
  22. package/src/agent_run.tsx +1 -1
  23. package/src/capture_row.tsx +59 -0
  24. package/src/change_review.tsx +732 -236
  25. package/src/checklist.tsx +104 -0
  26. package/src/chip.tsx +12 -3
  27. package/src/composer.tsx +22 -1
  28. package/src/confidence.tsx +1 -1
  29. package/src/control_surface.ts +0 -14
  30. package/src/detail_row.tsx +137 -10
  31. package/src/finding.tsx +112 -80
  32. package/src/floating_action_bar.tsx +1 -1
  33. package/src/inline_date_picker.tsx +8 -3
  34. package/src/inline_edit.tsx +40 -10
  35. package/src/inline_member_select.tsx +3 -0
  36. package/src/inline_number_input.tsx +5 -2
  37. package/src/inline_select.tsx +8 -3
  38. package/src/inline_tag_select.tsx +140 -0
  39. package/src/inline_text_input.tsx +5 -2
  40. package/src/inline_time_picker.tsx +5 -2
  41. package/src/ledger.tsx +220 -0
  42. package/src/locale.tsx +43 -16
  43. package/src/progress_bar.tsx +32 -1
  44. package/src/record_summary.tsx +101 -0
  45. package/src/section_heading.tsx +16 -8
  46. package/src/sources.tsx +8 -5
  47. package/src/suggestion_chip.tsx +47 -0
  48. package/src/use_section_nav.test.ts +69 -0
  49. package/src/use_section_nav.ts +59 -0
  50. package/examples/tpl_assistant.tsx +0 -174
  51. package/examples/tpl_billing.tsx +0 -344
  52. package/examples/tpl_briefing.tsx +0 -121
  53. package/examples/tpl_compare.tsx +0 -133
  54. package/examples/tpl_crosscheck.tsx +0 -120
  55. package/examples/tpl_detail.tsx +0 -232
  56. package/examples/tpl_directory.tsx +0 -260
  57. package/examples/tpl_draft.tsx +0 -163
  58. package/examples/tpl_extract.tsx +0 -193
  59. package/examples/tpl_intake.tsx +0 -206
  60. package/examples/tpl_match.tsx +0 -134
  61. package/examples/tpl_order.tsx +0 -482
  62. package/examples/tpl_quick.tsx +0 -211
  63. package/examples/tpl_ratedesk.tsx +0 -386
  64. package/examples/tpl_record_plain.tsx +0 -259
  65. package/examples/tpl_settings.tsx +0 -178
  66. package/examples/tpl_timeline.tsx +0 -244
  67. package/examples/tpl_triage.tsx +0 -112
  68. package/examples/tpl_wizard.tsx +0 -223
  69. package/src/discrepancy.tsx +0 -114
  70. package/src/match_sides.tsx +0 -79
  71. package/src/record_fields.tsx +0 -68
  72. package/src/review_card.tsx +0 -172
  73. package/src/scored_option.tsx +0 -138
  74. package/src/spec_list.tsx +0 -81
  75. package/src/triage_row.tsx +0 -99
@@ -7,7 +7,8 @@ import { DatePickerPanel } from "./date_picker";
7
7
  import { isoHasTime } from "./date_picker_value";
8
8
  import { formatDate } from "./format_date";
9
9
  import { ActivityIndicator } from "./activity_indicator";
10
- import { InlineEditView } from "./inline_edit";
10
+ import { type InlineEditBackground, InlineEditView } from "./inline_edit";
11
+ import { useLoticsLocale } from "./locale";
11
12
 
12
13
  export interface InlineDatePickerProps {
13
14
  /** ISO date (`2026-05-22`) or datetime (`2026-05-22T14:30`). */
@@ -23,6 +24,8 @@ export interface InlineDatePickerProps {
23
24
  locale?: string;
24
25
  disabled?: boolean;
25
26
  accessibilityLabel?: string;
27
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
28
+ background?: InlineEditBackground;
26
29
  }
27
30
 
28
31
  /**
@@ -33,7 +36,8 @@ export interface InlineDatePickerProps {
33
36
  * dismissing without a change reverts.
34
37
  */
35
38
  export function InlineDatePicker(props: InlineDatePickerProps) {
36
- const { value, onSave, format = "date", optionalTime, placeholder, locale, disabled, accessibilityLabel } = props;
39
+ const { value, onSave, format = "date", optionalTime, placeholder, locale, disabled, accessibilityLabel , background } = props;
40
+ const labels = useLoticsLocale().inline;
37
41
  const [open, setOpen] = useState(false);
38
42
  const [draft, setDraft] = useState<string | null>(value);
39
43
  const [saving, setSaving] = useState(false);
@@ -50,7 +54,7 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
50
54
  try {
51
55
  await onSave(next);
52
56
  } catch (e) {
53
- setError(e instanceof Error && e.message ? e.message : "Couldn't save. Try again.");
57
+ setError(e instanceof Error && e.message ? e.message : labels.saveError);
54
58
  } finally {
55
59
  setSaving(false);
56
60
  }
@@ -81,6 +85,7 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
81
85
  <Popover open={open && !disabled} onOpenChange={onOpenChange} side="bottom" align="start">
82
86
  <PopoverTrigger>
83
87
  <InlineEditView
88
+ background={background}
84
89
  display={display}
85
90
  placeholder={placeholder}
86
91
  disabled={disabled}
@@ -2,6 +2,7 @@ import { useCallback, useRef, useState, type ReactNode, type Ref } from "react";
2
2
  import { View, StyleSheet, type GestureResponderEvent, type TextStyle } from "react-native";
3
3
  import { Text } from "./text";
4
4
  import { IconButton } from "./icon_button";
5
+ import { useLoticsLocale } from "./locale";
5
6
  import { ActivityIndicator } from "./activity_indicator";
6
7
  import { FocusRingPressable } from "./focus_ring_pressable";
7
8
  import { colors } from "./colors";
@@ -28,6 +29,7 @@ export function useInlineEdit<T>(opts: {
28
29
  equals?: (a: T, b: T) => boolean;
29
30
  }) {
30
31
  const { value, onSave, equals } = opts;
32
+ const labels = useLoticsLocale().inline;
31
33
  const [editing, setEditing] = useState(false);
32
34
  const [draft, setDraft] = useState<T>(value);
33
35
  const [saving, setSaving] = useState(false);
@@ -69,7 +71,7 @@ export function useInlineEdit<T>(opts: {
69
71
  setEditing(false);
70
72
  } catch (e) {
71
73
  // Stay in edit mode so the entry isn't lost — show the error, re-arm.
72
- setError(e instanceof Error && e.message ? e.message : "Couldn't save. Try again.");
74
+ setError(e instanceof Error && e.message ? e.message : labels.saveError);
73
75
  active.current = true;
74
76
  } finally {
75
77
  setSaving(false);
@@ -81,6 +83,13 @@ export function useInlineEdit<T>(opts: {
81
83
  return { editing, draft, setDraft, saving, error, begin, cancel, commit };
82
84
  }
83
85
 
86
+ /** The resting surface of an inline editor. "tint" (default) is the zinc-50
87
+ * chip — THE editability affordance on surfaces that MIX editable and static
88
+ * values. "transparent" drops it for DENSE, uniformly-editable surfaces (a
89
+ * task list/board where every cell edits — the chip repeated everywhere is
90
+ * noise, and hover/focus still reveal the input). */
91
+ export type InlineEditBackground = "tint" | "transparent";
92
+
84
93
  interface InlineEditFrameProps {
85
94
  editing: boolean;
86
95
  /** The formatted current value, shown in view mode. Empty → placeholder. */
@@ -101,6 +110,8 @@ interface InlineEditFrameProps {
101
110
  accessibilityLabel?: string;
102
111
  /** Strike + mute the resting value (a completed item that stays editable). */
103
112
  struck?: boolean;
113
+ /** Resting surface — see {@link InlineEditBackground}. Default "tint". */
114
+ background?: InlineEditBackground;
104
115
  }
105
116
 
106
117
  interface InlineEditViewProps {
@@ -119,20 +130,23 @@ interface InlineEditViewProps {
119
130
  active?: boolean;
120
131
  /** Strike + mute the resting value (a completed/superseded item that stays editable). */
121
132
  struck?: boolean;
133
+ /** Resting surface — see {@link InlineEditBackground}. Default "tint". */
134
+ background?: InlineEditBackground;
122
135
  ref?: Ref<View>;
123
136
  }
124
137
 
125
138
  /**
126
139
  * The view-mode box of an inline-editable field: the value as plain text in a
127
140
  * `FocusRingPressable` (an input, so it hovers via its BORDER — the transparent
128
- * edge reveals on hover, never a grey wash; the pointer cursor signals it's
129
- * editable), at the kit's control height with a transparent border so
141
+ * edge reveals on hover, never a grey wash; the cursor stays the default arrow —
142
+ * the zinc-50 chip + hover-border are the affordance, a pointer would read as a
143
+ * button), at the kit's control height with a transparent border — so
130
144
  * swapping to an input, or floating a dropdown above it, never shifts the
131
145
  * layout. Used by `InlineEditFrame`, and as the overlay trigger for the
132
146
  * select/date inline editors (it forwards ref + onPress to a `PopoverTrigger`).
133
147
  */
134
148
  export function InlineEditView(props: InlineEditViewProps) {
135
- const { display, placeholder, onPress, disabled, accessibilityLabel, trailing, active, struck, ref } = props;
149
+ const { display, placeholder, onPress, disabled, accessibilityLabel, trailing, active, struck, background = "tint", ref } = props;
136
150
  // Stop the press here so an inline editor nested in a pressable row (a task
137
151
  // row that expands on press) edits the field instead of triggering the row.
138
152
  const handlePress = onPress
@@ -152,7 +166,7 @@ export function InlineEditView(props: InlineEditViewProps) {
152
166
  // An inline-edit field is an input — it hovers via its BORDER, not a grey
153
167
  // wash: the transparent edge reveals on hover (the kit's hover-border),
154
168
  // then fills + rings when open. Suppressed while open (the ring leads).
155
- style={(state) => [styles.view, CONTROL_TRANSITION, active && styles.viewActive, !active && state.hovered && !disabled && styles.viewHovered]}
169
+ style={(state) => [styles.view, (background === "transparent" || disabled) && styles.viewTransparent, CONTROL_TRANSITION, active && styles.viewActive, !active && state.hovered && !disabled && styles.viewHovered]}
156
170
  >
157
171
  {typeof display === "string" ? (
158
172
  <Text numberOfLines={1} style={[viewTextStyle, display ? null : styles.placeholder, struck ? styles.struck : null]}>
@@ -174,6 +188,7 @@ export function InlineEditView(props: InlineEditViewProps) {
174
188
  * directly as a `Popover` trigger instead.)
175
189
  */
176
190
  export function InlineEditFrame(props: InlineEditFrameProps) {
191
+ const labels = useLoticsLocale().inline;
177
192
  const {
178
193
  editing,
179
194
  display,
@@ -188,11 +203,13 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
188
203
  disabled,
189
204
  accessibilityLabel,
190
205
  struck,
206
+ background,
191
207
  } = props;
192
208
 
193
209
  if (!editing) {
194
210
  return (
195
211
  <InlineEditView
212
+ background={background}
196
213
  display={display}
197
214
  placeholder={placeholder}
198
215
  onPress={onBegin}
@@ -218,8 +235,8 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
218
235
  </View>
219
236
  {controls === "buttons" ? (
220
237
  <View style={styles.buttons}>
221
- <IconButton icon="check" color="primary" size="sm" tooltip="Save" onPress={onCommit} disabled={saving} />
222
- <IconButton icon="x" color="secondary" size="sm" tooltip="Cancel" onPress={onCancel} disabled={saving} />
238
+ <IconButton icon="check" color="primary" size="sm" tooltip={labels.save} onPress={onCommit} disabled={saving} />
239
+ <IconButton icon="x" color="secondary" size="sm" tooltip={labels.cancel} onPress={onCancel} disabled={saving} />
223
240
  </View>
224
241
  ) : null}
225
242
  </View>
@@ -247,24 +264,37 @@ export const inlineValueTextStyle: TextStyle = {
247
264
  const viewTextStyle = [inlineValueTextStyle, { flex: 1 }];
248
265
 
249
266
  const styles = StyleSheet.create({
267
+ // At REST an editor sits on a zinc-50 chip — THE editability affordance: a
268
+ // read-only `InlineStatic` stays flat, so editable and static values are
269
+ // distinguishable without hovering (they were pixel-identical before, and
270
+ // users could not discover what was editable).
250
271
  view: {
251
272
  minHeight: INLINE_CONTROL_HEIGHT,
252
273
  borderRadius: CONTROL_RADIUS,
253
274
  borderWidth: 1,
254
275
  borderColor: "transparent",
276
+ backgroundColor: colors.zinc[50],
255
277
  paddingHorizontal: 8,
256
278
  flexDirection: "row",
257
279
  alignItems: "center",
258
280
  gap: 6,
281
+ // An inline editor is an INPUT, not a button — keep the default arrow
282
+ // cursor (the chip + hover-border carry the affordance; a pointer reads
283
+ // as a button press). Overrides the Pressable's pointer default.
284
+ cursor: "auto",
259
285
  },
286
+ // The transparent opt-out — dense, uniformly-editable surfaces.
287
+ viewTransparent: { backgroundColor: "transparent" },
260
288
  // Open (popover showing): the transparent edge fills in to the kit's 1px border
261
- // under the 2px active ring — identical to a focused input.
289
+ // under the 2px active ring — identical to a focused input; the chip clears to
290
+ // the input's white surface.
262
291
  viewActive: {
263
292
  borderColor: colors.border,
293
+ backgroundColor: colors.white,
264
294
  boxShadow: FOCUS_RING,
265
295
  },
266
- // Hover (not open): reveal the kit hover-border on the transparent edge — the
267
- // input-family affordance, no grey wash.
296
+ // Hover (not open): reveal the kit hover-border on the chip — the input-family
297
+ // affordance.
268
298
  viewHovered: {
269
299
  borderColor: HOVER_BORDER,
270
300
  },
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useMemo } from "react";
2
+ import type { InlineEditBackground } from "./inline_edit";
2
3
  import { InlineSelect } from "./inline_select";
3
4
  import { MemberChip } from "./member_chip";
4
5
  import type { MemberSelectMember } from "./member_select";
@@ -18,6 +19,8 @@ interface InlineMemberSelectProps {
18
19
  placeholder?: string;
19
20
  disabled?: boolean;
20
21
  accessibilityLabel?: string;
22
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
23
+ background?: InlineEditBackground;
21
24
  /** Show an in-menu search box to filter the roster; default false. */
22
25
  searchable?: boolean;
23
26
  }
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import type { KeyboardEvent } from "react";
3
3
  import { NumberInput } from "./number_input";
4
- import { InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
4
+ import { type InlineEditBackground, InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
5
5
 
6
6
  export interface InlineNumberInputProps {
7
7
  value: number | null;
@@ -15,6 +15,8 @@ export interface InlineNumberInputProps {
15
15
  controls?: InlineEditControls;
16
16
  disabled?: boolean;
17
17
  accessibilityLabel?: string;
18
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
19
+ background?: InlineEditBackground;
18
20
  }
19
21
 
20
22
  /**
@@ -23,7 +25,7 @@ export interface InlineNumberInputProps {
23
25
  * the bare numeric field at the same height, so the form never reflows.
24
26
  */
25
27
  export function InlineNumberInput(props: InlineNumberInputProps) {
26
- const { value, onSave, format, placeholder, min, max, controls = "blur", disabled, accessibilityLabel } = props;
28
+ const { value, onSave, format, placeholder, min, max, controls = "blur", disabled, accessibilityLabel , background } = props;
27
29
  const edit = useInlineEdit<number | null>({ value, onSave });
28
30
 
29
31
  const onKeyDown = useCallback(
@@ -43,6 +45,7 @@ export function InlineNumberInput(props: InlineNumberInputProps) {
43
45
 
44
46
  return (
45
47
  <InlineEditFrame
48
+ background={background}
46
49
  editing={edit.editing}
47
50
  display={display}
48
51
  placeholder={placeholder}
@@ -7,7 +7,8 @@ import { Popover, PopoverTrigger, PopoverContent } from "./popover";
7
7
  import { OptionList } from "./option_list";
8
8
  import type { PickerOption } from "./picker";
9
9
  import { ActivityIndicator } from "./activity_indicator";
10
- import { InlineEditView } from "./inline_edit";
10
+ import { type InlineEditBackground, InlineEditView } from "./inline_edit";
11
+ import { useLoticsLocale } from "./locale";
11
12
 
12
13
  export interface InlineSelectProps<T extends string> {
13
14
  value: T | null;
@@ -24,6 +25,8 @@ export interface InlineSelectProps<T extends string> {
24
25
  placeholder?: string;
25
26
  disabled?: boolean;
26
27
  accessibilityLabel?: string;
28
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
29
+ background?: InlineEditBackground;
27
30
  /** Show an in-menu search box; default false. */
28
31
  searchable?: boolean;
29
32
  }
@@ -36,7 +39,8 @@ export interface InlineSelectProps<T extends string> {
36
39
  * Pass `renderOptionContent` for rich options, or omit it for a plain label list.
37
40
  */
38
41
  export function InlineSelect<T extends string>(props: InlineSelectProps<T>) {
39
- const { value, onSave, options, renderOptionContent, renderSelected, placeholder, disabled, accessibilityLabel, searchable = false } = props;
42
+ const { value, onSave, options, renderOptionContent, renderSelected, placeholder, disabled, accessibilityLabel, searchable = false, background } = props;
43
+ const labels = useLoticsLocale().inline;
40
44
  const [open, setOpen] = useState(false);
41
45
  const [saving, setSaving] = useState(false);
42
46
  const [error, setError] = useState<string | null>(null);
@@ -52,7 +56,7 @@ export function InlineSelect<T extends string>(props: InlineSelectProps<T>) {
52
56
  try {
53
57
  await onSave(next);
54
58
  } catch (e) {
55
- setError(e instanceof Error && e.message ? e.message : "Couldn't save. Try again.");
59
+ setError(e instanceof Error && e.message ? e.message : labels.saveError);
56
60
  } finally {
57
61
  setSaving(false);
58
62
  }
@@ -65,6 +69,7 @@ export function InlineSelect<T extends string>(props: InlineSelectProps<T>) {
65
69
  <Popover open={open && !disabled} onOpenChange={setOpen} side="bottom" align="start" inheritTriggerWidth>
66
70
  <PopoverTrigger>
67
71
  <InlineEditView
72
+ background={background}
68
73
  display={selected ? (renderSelected ? renderSelected(selected) : renderOptionContent ? renderOptionContent(selected) : (selected.label ?? "")) : ""}
69
74
  placeholder={placeholder}
70
75
  disabled={disabled}
@@ -0,0 +1,140 @@
1
+ import { useState } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import { Icon } from "./icon";
4
+ import { Text } from "./text";
5
+ import { colors } from "./colors";
6
+ import { Popover, PopoverTrigger, PopoverContent } from "./popover";
7
+ import { OptionList } from "./option_list";
8
+ import type { PickerOption } from "./picker";
9
+ import { ActivityIndicator } from "./activity_indicator";
10
+ import { Badge } from "./badge";
11
+ import { type InlineEditBackground, InlineEditView } from "./inline_edit";
12
+ import { useLoticsLocale } from "./locale";
13
+
14
+ export interface InlineTagSelectProps<T extends string> {
15
+ /** The selected tag values. */
16
+ value: T[];
17
+ /** Commits the NEW SET when the picker closes (only if it changed). Throwing
18
+ * surfaces the inline error, like every inline editor. */
19
+ onSave: (next: T[]) => void | Promise<void>;
20
+ options: PickerOption<T>[];
21
+ /** The resting render of ONE selected tag (an `OptionBadge` dot, a `Chip`).
22
+ * Defaults to a plain zinc `Badge` of the label. */
23
+ renderTag?: (option: PickerOption<T>) => React.ReactNode;
24
+ /** Rich row content in the dropdown. Falls back to `renderTag`, then label. */
25
+ renderOptionContent?: (option: PickerOption<T>) => React.ReactNode;
26
+ placeholder?: string;
27
+ disabled?: boolean;
28
+ accessibilityLabel?: string;
29
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
30
+ background?: InlineEditBackground;
31
+ /** Show an in-menu search box; default false. */
32
+ searchable?: boolean;
33
+ }
34
+
35
+ /**
36
+ * The inline-editable MULTI select — the tag-field member of the `Inline*`
37
+ * family (`InlineSelect` picks one; this holds a set). At rest the selected
38
+ * tags render as badges inside the standard inline chip; clicking floats a
39
+ * multi `OptionList` (checkbox rows) in a popover, toggles edit a local draft,
40
+ * and CLOSING the popover commits the new set in one `onSave` — the blur-commit
41
+ * contract of the inline family, applied to a set.
42
+ */
43
+ export function InlineTagSelect<T extends string>(props: InlineTagSelectProps<T>) {
44
+ const {
45
+ value, onSave, options, renderTag, renderOptionContent, placeholder, disabled, accessibilityLabel, background, searchable = false,
46
+ } = props;
47
+ const labels = useLoticsLocale().inline;
48
+ const [open, setOpenState] = useState(false);
49
+ const [draft, setDraft] = useState<T[]>(value);
50
+ const [saving, setSaving] = useState(false);
51
+ const [error, setError] = useState<string | null>(null);
52
+
53
+ const commit = async (next: T[]) => {
54
+ const changed = next.length !== value.length || next.some((v) => !value.includes(v));
55
+ if (!changed) return;
56
+ setSaving(true);
57
+ setError(null);
58
+ try {
59
+ await onSave(next);
60
+ } catch (e) {
61
+ setError(e instanceof Error && e.message ? e.message : labels.saveError);
62
+ } finally {
63
+ setSaving(false);
64
+ }
65
+ };
66
+
67
+ const setOpen = (next: boolean) => {
68
+ if (next) {
69
+ setDraft(value);
70
+ setOpenState(true);
71
+ return;
72
+ }
73
+ setOpenState(false);
74
+ void commit(draft);
75
+ };
76
+
77
+ const selected = options.filter((o) => value.includes(o.value));
78
+ const tag = (o: PickerOption<T>) => (renderTag ? renderTag(o) : <Badge label={o.label ?? String(o.value)} color="zinc" />);
79
+
80
+ return (
81
+ <View>
82
+ <Popover open={open && !disabled} onOpenChange={setOpen} side="bottom" align="start" inheritTriggerWidth>
83
+ <PopoverTrigger>
84
+ <InlineEditView
85
+ background={background}
86
+ display={
87
+ selected.length > 0 ? (
88
+ <View style={styles.tags}>
89
+ {selected.map((o) => (
90
+ <View key={o.value}>{tag(o)}</View>
91
+ ))}
92
+ </View>
93
+ ) : (
94
+ ""
95
+ )
96
+ }
97
+ placeholder={placeholder}
98
+ disabled={disabled}
99
+ active={open && !disabled}
100
+ accessibilityLabel={accessibilityLabel}
101
+ trailing={
102
+ saving ? (
103
+ <ActivityIndicator size={16} color={colors.zinc[400]} />
104
+ ) : (
105
+ <Icon name="chevron-down" size={18} color={colors.zinc[400]} />
106
+ )
107
+ }
108
+ />
109
+ </PopoverTrigger>
110
+ <PopoverContent disableBodyScroll>
111
+ <OptionList<T, true>
112
+ multi
113
+ search={{ mode: searchable ? "internal" : "none" }}
114
+ options={options}
115
+ value={draft}
116
+ onValueChange={setDraft}
117
+ onRequestClose={() => setOpen(false)}
118
+ renderOptionContent={renderOptionContent ?? renderTag}
119
+ />
120
+ </PopoverContent>
121
+ </Popover>
122
+ {error ? (
123
+ <Text size="xs" color="danger" style={styles.error}>
124
+ {error}
125
+ </Text>
126
+ ) : null}
127
+ </View>
128
+ );
129
+ }
130
+
131
+ const styles = StyleSheet.create({
132
+ tags: {
133
+ flexDirection: "row",
134
+ alignItems: "center",
135
+ flexWrap: "wrap",
136
+ columnGap: 4,
137
+ rowGap: 2,
138
+ },
139
+ error: { marginTop: 4 },
140
+ });
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import type { NativeSyntheticEvent, TextInputKeyPressEventData } from "react-native";
3
3
  import { TextInputField } from "./text_input_field";
4
- import { InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
4
+ import { type InlineEditBackground, InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
5
5
 
6
6
  export interface InlineTextInputProps {
7
7
  value: string;
@@ -15,6 +15,8 @@ export interface InlineTextInputProps {
15
15
  disabled?: boolean;
16
16
  /** Strike + mute the resting value (a completed item that stays editable). */
17
17
  struck?: boolean;
18
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
19
+ background?: InlineEditBackground;
18
20
  accessibilityLabel?: string;
19
21
  }
20
22
 
@@ -25,7 +27,7 @@ export interface InlineTextInputProps {
25
27
  * value in a dense record / detail surface.
26
28
  */
27
29
  export function InlineTextInput(props: InlineTextInputProps) {
28
- const { value, onSave, placeholder, controls = "blur", disabled, struck, accessibilityLabel } = props;
30
+ const { value, onSave, placeholder, controls = "blur", disabled, struck, accessibilityLabel , background } = props;
29
31
  const edit = useInlineEdit<string>({ value, onSave });
30
32
 
31
33
  const onKeyPress = useCallback(
@@ -47,6 +49,7 @@ export function InlineTextInput(props: InlineTextInputProps) {
47
49
 
48
50
  return (
49
51
  <InlineEditFrame
52
+ background={background}
50
53
  editing={edit.editing}
51
54
  display={value}
52
55
  placeholder={placeholder}
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import type { KeyboardEvent } from "react";
3
3
  import { TimePicker } from "./time_picker";
4
- import { InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
4
+ import { type InlineEditBackground, InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
5
5
 
6
6
  export interface InlineTimePickerProps {
7
7
  /** Canonical 24-hour "HH:mm", "" when empty. */
@@ -13,6 +13,8 @@ export interface InlineTimePickerProps {
13
13
  controls?: InlineEditControls;
14
14
  disabled?: boolean;
15
15
  accessibilityLabel?: string;
16
+ /** Resting surface — "tint" (default, the zinc-50 chip) or "transparent". */
17
+ background?: InlineEditBackground;
16
18
  }
17
19
 
18
20
  /**
@@ -21,7 +23,7 @@ export interface InlineTimePickerProps {
21
23
  * field at the same height, so the form never reflows.
22
24
  */
23
25
  export function InlineTimePicker(props: InlineTimePickerProps) {
24
- const { value, onSave, placeholder, controls = "blur", disabled, accessibilityLabel } = props;
26
+ const { value, onSave, placeholder, controls = "blur", disabled, accessibilityLabel , background } = props;
25
27
  const edit = useInlineEdit<string>({ value, onSave });
26
28
 
27
29
  const onKeyDown = useCallback(
@@ -39,6 +41,7 @@ export function InlineTimePicker(props: InlineTimePickerProps) {
39
41
 
40
42
  return (
41
43
  <InlineEditFrame
44
+ background={background}
42
45
  editing={edit.editing}
43
46
  display={value}
44
47
  placeholder={placeholder}