@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.
- package/AGENTS.md +298 -159
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_item_list.tsx +1015 -124
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +1354 -0
- package/examples/tpl_report.tsx +7 -7
- package/examples/tpl_rollup.tsx +6 -6
- package/examples/tpl_shifts.tsx +2 -2
- package/examples/tpl_statements.tsx +221 -0
- package/examples/tpl_stock.tsx +7 -7
- package/examples/tpl_task_board.tsx +48 -32
- package/examples/tpl_tasks.tsx +47 -47
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +9 -9
- package/src/agent_run.tsx +1 -1
- package/src/capture_row.tsx +59 -0
- package/src/change_review.tsx +732 -236
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/detail_row.tsx +137 -10
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/inline_date_picker.tsx +8 -3
- package/src/inline_edit.tsx +40 -10
- package/src/inline_member_select.tsx +3 -0
- package/src/inline_number_input.tsx +5 -2
- package/src/inline_select.tsx +8 -3
- package/src/inline_tag_select.tsx +140 -0
- package/src/inline_text_input.tsx +5 -2
- package/src/inline_time_picker.tsx +5 -2
- package/src/ledger.tsx +220 -0
- package/src/locale.tsx +43 -16
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/sources.tsx +8 -5
- package/src/suggestion_chip.tsx +47 -0
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_record_plain.tsx +0 -259
- package/examples/tpl_settings.tsx +0 -178
- package/examples/tpl_timeline.tsx +0 -244
- package/examples/tpl_triage.tsx +0 -112
- package/examples/tpl_wizard.tsx +0 -223
- package/src/discrepancy.tsx +0 -114
- package/src/match_sides.tsx +0 -79
- package/src/record_fields.tsx +0 -68
- package/src/review_card.tsx +0 -172
- package/src/scored_option.tsx +0 -138
- package/src/spec_list.tsx +0 -81
- 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 :
|
|
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}
|
package/src/inline_edit.tsx
CHANGED
|
@@ -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 :
|
|
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
|
|
129
|
-
*
|
|
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=
|
|
222
|
-
<IconButton icon="x" color="secondary" size="sm" tooltip=
|
|
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
|
|
267
|
-
//
|
|
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}
|
package/src/inline_select.tsx
CHANGED
|
@@ -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 :
|
|
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}
|