@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
@@ -12,13 +12,13 @@ import { InlineTextInput } from "@lotics/ui/inline_text_input";
12
12
  import { InlineDatePicker } from "@lotics/ui/inline_date_picker";
13
13
  import { InlineSelect } from "@lotics/ui/inline_select";
14
14
  import { Select } from "@lotics/ui/select";
15
- import { TextInputField } from "@lotics/ui/text_input_field";
15
+ import { CaptureRow } from "@lotics/ui/capture_row";
16
16
  import { Button } from "@lotics/ui/button";
17
17
  import { FilesEditor } from "@lotics/ui/files_editor";
18
18
  import type { DisplayFile } from "@lotics/ui/file_thumbnail";
19
19
  import { Composer } from "@lotics/ui/composer";
20
20
  import { AgentRun, type AgentRunItem } from "@lotics/ui/agent_run";
21
- import { ChangeReview, ChangeReviewActions, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
21
+ import { Change, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeSummary, type ChangeStatus } from "@lotics/ui/change_review";
22
22
  import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
23
23
  import { CompletionState } from "@lotics/ui/completion_state";
24
24
  import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
@@ -27,7 +27,7 @@ import { formatDate, toISODate } from "@lotics/ui/format_date";
27
27
  type TagOption = { value: string; label: string };
28
28
 
29
29
  // ─────────────────────────────────────────────────────────────────────────────
30
- // Template · Tasks (quick list) — the Apple-Reminders shape for PERSONAL lists.
30
+ // Template · Task list — the Apple-Reminders shape for PERSONAL lists.
31
31
  // There's no Task component: a row is composed directly from primitives
32
32
  // (CheckCircle + an inline-editable title + a quiet meta line), which is the most
33
33
  // flexible way to build a task surface. Quiet, scannable rows you tick to finish
@@ -160,7 +160,7 @@ export function TplTasks() {
160
160
  const [phase, setPhase] = useState<Phase>("idle");
161
161
  const [revealed, setRevealed] = useState(0);
162
162
  const [proposals, setProposals] = useState<Proposal[]>([]);
163
- const [decisions, setDecisions] = useState<Record<string, ChangeReviewItemStatus>>({});
163
+ const [decisions, setDecisions] = useState<Record<string, ChangeStatus>>({});
164
164
  const [addedCount, setAddedCount] = useState(0);
165
165
 
166
166
  const patch = (id: string, next: Partial<Task>) =>
@@ -182,7 +182,7 @@ export function TplTasks() {
182
182
  const closeDraft = useCallback(() => { setDraftOpen(false); resetDraft(); }, [resetDraft]);
183
183
  const editProposal = useCallback((id: string, p: Partial<Proposal>) =>
184
184
  setProposals((ps) => ps.map((x) => (x.id === id ? { ...x, ...p } : x))), []);
185
- const decide = useCallback((id: string, status: ChangeReviewItemStatus | null) =>
185
+ const decide = useCallback((id: string, status: ChangeStatus | null) =>
186
186
  setDecisions((dec) => { const n = { ...dec }; if (status) n[id] = status; else delete n[id]; return n; }), []);
187
187
  const acceptedCount = proposals.filter((p) => decisions[p.id] === "accepted").length;
188
188
  const addAccepted = useCallback(() => {
@@ -259,15 +259,7 @@ export function TplTasks() {
259
259
 
260
260
  {/* Capture — type to add; the submit button lives inside the field on the right. */}
261
261
  <View style={styles.capture}>
262
- <View style={[styles.row, { gap: 12 }]}>
263
- <View style={styles.emptyRing} />
264
- <View style={styles.captureField}>
265
- <View style={{ flex: 1 }}>
266
- <TextInputField value={draft} onChangeText={setDraft} onSubmitEditing={add} placeholder="Add a task…" style={styles.titleInput} accessibilityLabel="Add a task" />
267
- </View>
268
- {draft.trim() ? <Button title="Save" color="primary" onPress={add} /> : null}
269
- </View>
270
- </View>
262
+ <CaptureRow value={draft} onChangeText={setDraft} onSubmit={add} placeholder="Add a task…" accessibilityLabel="Add a task" />
271
263
  </View>
272
264
 
273
265
  <View>
@@ -294,7 +286,7 @@ export function TplTasks() {
294
286
  <View style={styles.row}>
295
287
  <CheckCircle checked={done} onChange={(on) => patch(t.id, { status: on ? "done" : "todo" })} accessibilityLabel={t.title} />
296
288
  <View style={{ flex: 1 }}>
297
- <InlineTextInput value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={done} accessibilityLabel="Title" />
289
+ <InlineTextInput background="transparent" value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={done} accessibilityLabel="Title" />
298
290
  </View>
299
291
  <Pressable
300
292
  onPress={() => toggleExpand(t.id, !open)}
@@ -320,16 +312,16 @@ export function TplTasks() {
320
312
  {open ? (
321
313
  <View style={styles.detail}>
322
314
  <Field label="Due">
323
- <InlineDatePicker value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} placeholder="Set a due date" accessibilityLabel="Due date" />
315
+ <InlineDatePicker background="transparent" value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} placeholder="Set a due date" accessibilityLabel="Due date" />
324
316
  </Field>
325
317
  <Field label="Status">
326
- <InlineSelect value={t.status} options={STATUS_OPTIONS} onSave={(st) => patch(t.id, { status: st })} renderSelected={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} renderOptionContent={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} accessibilityLabel="Status" />
318
+ <InlineSelect background="transparent" value={t.status} options={STATUS_OPTIONS} onSave={(st) => patch(t.id, { status: st })} renderSelected={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} renderOptionContent={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} accessibilityLabel="Status" />
327
319
  </Field>
328
320
  <Field label="Tags">
329
321
  <Select multi searchable allowCustom value={t.tags.map((tag) => tag.value)} onValueChange={(next) => patch(t.id, { tags: next.map(tagOf) })} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} style={{ borderColor: "transparent" }} placeholder="Add tags" accessibilityLabel="Tags" />
330
322
  </Field>
331
323
  <Field label="Note">
332
- <InlineTextInput value={t.note} onSave={(v) => patch(t.id, { note: v })} placeholder="Add a note…" accessibilityLabel="Note" />
324
+ <InlineTextInput background="transparent" value={t.note} onSave={(v) => patch(t.id, { note: v })} placeholder="Add a note…" accessibilityLabel="Note" />
333
325
  </Field>
334
326
  <Field label="Files" top>
335
327
  <FilesEditor files={t.files} itemSize={72} onAdd={(picked) => addFiles(t.id, picked)} onRemove={(id) => removeFile(t.id, id)} />
@@ -345,7 +337,10 @@ export function TplTasks() {
345
337
  </View>
346
338
  </ScrollView>
347
339
 
348
- {/* Draft from notes — Composer → AgentRun → ChangeReview → commit. */}
340
+ {/* Draft from notes — Composer → AgentRun → ChangeReview → commit. The review
341
+ provider wraps the WHOLE dialog so the `Change`s (scroll area) and the
342
+ commit bar (`DialogFooter`) share one review context. */}
343
+ <ChangeReview>
349
344
  <Dialog open={draftOpen} onOpenChange={(o) => { if (!o) closeDraft(); }} maxWidth={620}>
350
345
  <DialogHeader><DialogHeaderTitle>Draft from notes</DialogHeaderTitle></DialogHeader>
351
346
  <DialogScrollArea>
@@ -357,20 +352,29 @@ export function TplTasks() {
357
352
  ) : null}
358
353
  {phase === "reading" ? <AgentRun items={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
359
354
  {phase === "review" ? (
360
- <ChangeReview
361
- title="Drafted tasks"
362
- summary={`${proposals.length} from your notes — keep, edit, or drop each. A “Low” mark is worth a glance.`}
363
- items={proposals}
364
- getKey={(p) => p.id}
365
- statusOf={(p) => decisions[p.id] ?? "pending"}
366
- renderItem={(p) => <TaskProposal proposal={p} onEdit={editProposal} />}
367
- renderSummary={(p, kept) => (
368
- <Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={kept ? undefined : { textDecorationLine: "line-through" }}>{p.title}</Text>
369
- )}
370
- onAcceptItem={(i) => decide(proposals[i].id, "accepted")}
371
- onRejectItem={(i) => decide(proposals[i].id, "rejected")}
372
- onUndoItem={(i) => decide(proposals[i].id, null)}
373
- />
355
+ <View style={{ gap: 8 }}>
356
+ <ChangeReviewHeader title="Drafted tasks" />
357
+ <Text size="sm" color="muted">{`${proposals.length} from your notes — keep, edit, or drop each. A “Low” mark is worth a glance.`}</Text>
358
+ {proposals.map((p) => {
359
+ const status = decisions[p.id] ?? "pending";
360
+ const kept = status === "accepted";
361
+ return (
362
+ <Change
363
+ key={p.id}
364
+ id={p.id}
365
+ status={status}
366
+ onAccept={() => decide(p.id, "accepted")}
367
+ onReject={() => decide(p.id, "rejected")}
368
+ onUndo={() => decide(p.id, null)}
369
+ >
370
+ <TaskProposal proposal={p} onEdit={editProposal} />
371
+ <ChangeSummary>
372
+ <Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={kept ? undefined : { textDecorationLine: "line-through" }}>{p.title}</Text>
373
+ </ChangeSummary>
374
+ </Change>
375
+ );
376
+ })}
377
+ </View>
374
378
  ) : null}
375
379
  {phase === "done" ? (
376
380
  <CompletionState title={`${addedCount} ${addedCount === 1 ? "task" : "tasks"} added`} summary="They're in your list, grouped by due date." />
@@ -378,7 +382,7 @@ export function TplTasks() {
378
382
  </DialogScrollArea>
379
383
  {phase === "review" ? (
380
384
  <DialogFooter>
381
- <ChangeReviewActions items={proposals} statusOf={(p) => decisions[p.id] ?? "pending"} onAcceptItem={(i) => decide(proposals[i].id, "accepted")} onApply={addAccepted} onDiscard={resetDraft} applyLabel={`Add ${acceptedCount} ${acceptedCount === 1 ? "task" : "tasks"}`} />
385
+ <ChangeReviewActions onApply={addAccepted} onDiscard={resetDraft} applyLabel={`Add ${acceptedCount} ${acceptedCount === 1 ? "task" : "tasks"}`} />
382
386
  </DialogFooter>
383
387
  ) : phase === "done" ? (
384
388
  <DialogFooter>
@@ -387,6 +391,7 @@ export function TplTasks() {
387
391
  </DialogFooter>
388
392
  ) : null}
389
393
  </Dialog>
394
+ </ChangeReview>
390
395
  </>
391
396
  );
392
397
  }
@@ -396,12 +401,12 @@ function TaskProposal({ proposal, onEdit }: { proposal: Proposal; onEdit: (id: s
396
401
  <View style={{ gap: 10 }}>
397
402
  <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
398
403
  <View style={{ flex: 1 }}>
399
- <InlineTextInput value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
404
+ <InlineTextInput background="transparent" value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
400
405
  </View>
401
406
  <View style={{ paddingTop: 10 }}><Confidence level={proposal.confidence} /></View>
402
407
  </View>
403
408
  <View style={{ width: 200 }}>
404
- <InlineDatePicker value={proposal.due} onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
409
+ <InlineDatePicker background="transparent" value={proposal.due} onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
405
410
  </View>
406
411
  <View style={styles.source}><Text size="xs" color="muted">{proposal.source}</Text></View>
407
412
  </View>
@@ -429,25 +434,20 @@ const styles = StyleSheet.create({
429
434
  rowHover: { backgroundColor: colors.white },
430
435
  // The expand affordance — its own button (meta + chevron), sibling to the ring
431
436
  // and title so no interactive control nests inside another.
432
- expand: { flexDirection: "row", alignItems: "center", gap: 10, minHeight: 36, paddingHorizontal: 8, borderRadius: 8 },
437
+ // FIXED width so every title cell ends at the same x the meta content
438
+ // (clip count · tag dots · date) varies per row and right-aligns inside;
439
+ // without it the title editors read ragged/misaligned across rows.
440
+ expand: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 10, minHeight: 36, paddingHorizontal: 8, borderRadius: 8, width: 148 },
433
441
  capture: {
434
442
  borderBottomWidth: 1,
435
443
  borderBottomColor: colors.zinc[100],
436
444
  paddingBottom: 6,
437
445
  },
438
- // Borderless title input that matches an InlineTextInput's resting inset (8),
439
- // so the capture title lines up with the task titles below it.
440
- titleInput: {
441
- borderWidth: 0,
442
- backgroundColor: "transparent",
443
- paddingVertical: 0,
444
- },
445
- captureField: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 4 },
446
+
446
447
  sectionHead: { flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 6 },
447
448
  sectionLabel: { letterSpacing: 0.3, textTransform: "uppercase" },
448
- meta: { flexDirection: "row", alignItems: "center", gap: 10 },
449
+ meta: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 10, flex: 1 },
449
450
  clip: { flexDirection: "row", alignItems: "center", gap: 2 },
450
- emptyRing: { width: 20, height: 20, borderRadius: 10, borderWidth: 1.5, borderColor: colors.zinc[300], borderStyle: "dashed" },
451
451
  dots: { flexDirection: "row", alignItems: "center", gap: 4 },
452
452
  dot: { width: 8, height: 8, borderRadius: 999 },
453
453
  // ring (20) + gap (12) + the title's 8 inset → labels line up with the title text
@@ -15,7 +15,7 @@ import { StatusGrid, StatusLegend } from "@lotics/ui/status_grid";
15
15
  import { Timeline } from "@lotics/ui/timeline";
16
16
 
17
17
  // ─────────────────────────────────────────────────────────────────────────────
18
- // Template · Control tower — continuous monitoring of hundreds of live
18
+ // Template · Status grid — continuous monitoring of hundreds of live
19
19
  // units (machines, gates, chargers, sensors). Three bands answer the three
20
20
  // ops questions in order: KPI strip + StatusGrid = "is everything OK right
21
21
  // now?" (scan for red; the legend drills a state), exceptions rail =
@@ -200,7 +200,7 @@ export function TplTower() {
200
200
  return (
201
201
  <View key={site.key} style={{ gap: 8 }}>
202
202
  <View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
203
- <Text size="xs" color="muted" transform="uppercase">{site.label}</Text>
203
+ <Text size="xs" color="muted" weight="medium">{site.label}</Text>
204
204
  <Text size="xs" color="muted" tabular>{siteUnits.length}</Text>
205
205
  </View>
206
206
  <StatusGrid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "7.19.3",
3
+ "version": "9.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -30,6 +30,9 @@
30
30
  "./rotatable_image": "./src/rotatable_image.tsx",
31
31
  "./use_image_rotation": "./src/use_image_rotation.ts",
32
32
  "./use_selection": "./src/use_selection.ts",
33
+ "./use_section_nav": "./src/use_section_nav.ts",
34
+ "./ledger": "./src/ledger.tsx",
35
+ "./inline_tag_select": "./src/inline_tag_select.tsx",
33
36
  "./use_selection_mode": "./src/use_selection_mode.ts",
34
37
  "./file_thumbnail_grid": "./src/file_thumbnail_grid.tsx",
35
38
  "./uploading_thumbnail": "./src/uploading_thumbnail.tsx",
@@ -81,18 +84,11 @@
81
84
  },
82
85
  "./markdown.css": "./src/markdown.css",
83
86
  "./confidence": "./src/confidence.tsx",
84
- "./review_card": "./src/review_card.tsx",
85
87
  "./change_review": "./src/change_review.tsx",
88
+ "./finding": "./src/finding.tsx",
86
89
  "./clarify": "./src/clarify.tsx",
87
90
  "./choice_list": "./src/choice_list.tsx",
88
91
  "./sources": "./src/sources.tsx",
89
- "./record_fields": "./src/record_fields.tsx",
90
- "./spec_list": "./src/spec_list.tsx",
91
- "./match_sides": "./src/match_sides.tsx",
92
- "./finding": "./src/finding.tsx",
93
- "./discrepancy": "./src/discrepancy.tsx",
94
- "./triage_row": "./src/triage_row.tsx",
95
- "./scored_option": "./src/scored_option.tsx",
96
92
  "./icon": "./src/icon.tsx",
97
93
  "./dynamic_icon": {
98
94
  "react-native": "./src/dynamic_icon.tsx",
@@ -161,6 +157,10 @@
161
157
  "./table": "./src/table.tsx",
162
158
  "./data_grid": "./src/data_grid.tsx",
163
159
  "./detail_row": "./src/detail_row.tsx",
160
+ "./record_summary": "./src/record_summary.tsx",
161
+ "./capture_row": "./src/capture_row.tsx",
162
+ "./checklist": "./src/checklist.tsx",
163
+ "./suggestion_chip": "./src/suggestion_chip.tsx",
164
164
  "./danger_zone": "./src/danger_zone.tsx",
165
165
  "./scan_field": "./src/scan_field.tsx",
166
166
  "./completion_state": "./src/completion_state.tsx",
package/src/agent_run.tsx CHANGED
@@ -147,7 +147,7 @@ const INK = colors.zinc[700];
147
147
  * fires (not a growing stack of dots); once prose resumes the group settles into
148
148
  * one row — "{final action} · {n} steps" — that EXPANDS on press to the steps in
149
149
  * between. Text segments render as prose, the last one carrying a live caret. The
150
- * run always ends on the agent's text. Pair with `Composer` + `ReviewCard`.
150
+ * run always ends on the agent's text. Pair with `Composer` + `ChangeReview`.
151
151
  */
152
152
  export function AgentRun(props: AgentRunProps) {
153
153
  const { items, labelForTool, stepsLabel = (n) => `${n} steps`, accessibilityLabel } = props;
@@ -0,0 +1,59 @@
1
+ import { StyleSheet, View } from "react-native";
2
+ import { Button } from "./button";
3
+ import { colors } from "./colors";
4
+ import { useLoticsLocale } from "./locale";
5
+ import { TextInputField } from "./text_input_field";
6
+
7
+ export interface CaptureRowProps {
8
+ /** The draft text — controlled by the host list. */
9
+ value: string;
10
+ onChangeText: (next: string) => void;
11
+ /** Commit the draft (Enter, or the Save button that appears on type). The
12
+ * host trims/validates and clears `value`. */
13
+ onSubmit: () => void;
14
+ /** The invitation ("Add a task…"). */
15
+ placeholder?: string;
16
+ /** Save button title. Defaults to the locale's inline Save. */
17
+ saveLabel?: string;
18
+ accessibilityLabel: string;
19
+ }
20
+
21
+ /**
22
+ * The add-an-item row that closes an editable list: a dashed empty ring where
23
+ * the item's leading control will be, a borderless input on the same inset as
24
+ * the item titles above it, and a primary Save that appears once there's text.
25
+ * Enter commits too. One shape wherever a list grows in place — task
26
+ * checklists, simple item lists — so the affordance reads the same everywhere.
27
+ */
28
+ export function CaptureRow(props: CaptureRowProps) {
29
+ const { value, onChangeText, onSubmit, placeholder, saveLabel, accessibilityLabel } = props;
30
+ const labels = useLoticsLocale().inline;
31
+ return (
32
+ <View style={styles.row}>
33
+ <View style={styles.emptyRing} />
34
+ <View style={styles.field}>
35
+ <View style={styles.grow}>
36
+ <TextInputField
37
+ value={value}
38
+ onChangeText={onChangeText}
39
+ onSubmitEditing={onSubmit}
40
+ placeholder={placeholder}
41
+ style={styles.input}
42
+ accessibilityLabel={accessibilityLabel}
43
+ />
44
+ </View>
45
+ {value.trim() !== "" ? <Button title={saveLabel ?? labels.save} color="primary" onPress={onSubmit} /> : null}
46
+ </View>
47
+ </View>
48
+ );
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ row: { flexDirection: "row", alignItems: "center", gap: 12, minHeight: 32 },
53
+ emptyRing: { width: 20, height: 20, borderRadius: 10, borderWidth: 1.5, borderColor: colors.zinc[300], borderStyle: "dashed" },
54
+ field: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8 },
55
+ grow: { flex: 1 },
56
+ // Borderless, matching an inline title's resting inset so the invitation
57
+ // lines up with the item titles above.
58
+ input: { borderWidth: 0, backgroundColor: "transparent", paddingVertical: 0 },
59
+ });