@lotics/ui 22.3.0 → 23.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 (68) hide show
  1. package/AGENTS.md +9 -2
  2. package/MIGRATION.md +142 -2
  3. package/docs/ai_patterns.md +22 -22
  4. package/docs/catalog.md +119 -98
  5. package/docs/composition.md +134 -42
  6. package/docs/data_entry.md +67 -31
  7. package/docs/templates.md +86 -68
  8. package/examples/tpl_allocate.tsx +5 -5
  9. package/examples/tpl_attendance.tsx +2 -2
  10. package/examples/tpl_calendar.tsx +6 -6
  11. package/examples/tpl_dashboard.tsx +9 -9
  12. package/examples/tpl_dieline.tsx +2 -2
  13. package/examples/tpl_item_list.tsx +64 -37
  14. package/examples/tpl_lookup.tsx +5 -5
  15. package/examples/tpl_pick.tsx +6 -6
  16. package/examples/tpl_pivot.tsx +3 -3
  17. package/examples/tpl_record.tsx +919 -611
  18. package/examples/tpl_report.tsx +2 -2
  19. package/examples/tpl_rollup.tsx +5 -5
  20. package/examples/tpl_shifts.tsx +5 -5
  21. package/examples/tpl_statements.tsx +7 -7
  22. package/examples/tpl_stock.tsx +2 -2
  23. package/examples/tpl_task_board.tsx +42 -26
  24. package/examples/tpl_tower.tsx +6 -6
  25. package/package.json +3 -2
  26. package/src/agent_run.tsx +4 -4
  27. package/src/breakdown.tsx +1 -1
  28. package/src/calendar/calendar_view.tsx +1 -1
  29. package/src/change_review.tsx +9 -8
  30. package/src/chip_group.tsx +12 -2
  31. package/src/choice_list.tsx +2 -2
  32. package/src/confidence.tsx +2 -2
  33. package/src/data_grid.tsx +1 -1
  34. package/src/detail_row.tsx +50 -58
  35. package/src/file_dropzone.tsx +1 -1
  36. package/src/file_gallery_modal.tsx +3 -3
  37. package/src/file_rows.tsx +1 -1
  38. package/src/finding.tsx +4 -4
  39. package/src/form_field.tsx +1 -1
  40. package/src/format_date.ts +2 -2
  41. package/src/heatmap.tsx +1 -1
  42. package/src/inline_button.tsx +84 -0
  43. package/src/inline_date_picker.tsx +17 -10
  44. package/src/inline_edit.tsx +298 -59
  45. package/src/inline_member_select.tsx +8 -3
  46. package/src/inline_number_input.tsx +11 -4
  47. package/src/inline_select.tsx +26 -13
  48. package/src/inline_text_input.tsx +12 -4
  49. package/src/inline_time_picker.tsx +10 -4
  50. package/src/ledger.tsx +2 -2
  51. package/src/locale.tsx +7 -3
  52. package/src/matrix.tsx +1 -1
  53. package/src/number_input.tsx +18 -7
  54. package/src/pipeline.tsx +1 -1
  55. package/src/popover.tsx +1 -1
  56. package/src/press_door.tsx +1 -1
  57. package/src/pressable_highlight.tsx +1 -1
  58. package/src/progress_bar.tsx +3 -3
  59. package/src/record_summary.tsx +2 -2
  60. package/src/result_header.tsx +2 -2
  61. package/src/sequence.tsx +170 -0
  62. package/src/share_or_download.ts +2 -2
  63. package/src/step_progress.tsx +7 -5
  64. package/src/stepper.tsx +1 -1
  65. package/src/task.tsx +6 -6
  66. package/src/text_input_field.tsx +19 -1
  67. package/src/text_utils.ts +1 -1
  68. package/src/linked_record_box.tsx +0 -157
@@ -34,6 +34,17 @@ interface TextInputFieldProps extends RNTextInputProps {
34
34
  /** Accessible name for the clear button. Defaults to the locale's
35
35
  * `textInputField.clear` ("Clear" / "Xóa"). */
36
36
  clearLabel?: string;
37
+ /** The HOST paints the surface — drop this input's own border, fill, focus ring
38
+ * AND horizontal padding, so the page draws one box and the text lands on the
39
+ * host's inset rather than 8px further in. Set by an inline editor whose field
40
+ * carries `actions`: the field's shell owns the surface (so the verbs never
41
+ * move between modes) and owns the ring with it.
42
+ *
43
+ * HEIGHT and VERTICAL padding stay — those are what keep the field the same
44
+ * size in both modes; the horizontal inset is what has to be surrendered,
45
+ * because the host already spent `CONTROL_TEXT_INSET` on it and two insets
46
+ * stacked is a visible jump to the right on focus. */
47
+ seamless?: boolean;
37
48
  // DOM-only ARIA attrs not declared on React Native's TextInputProps. They
38
49
  // are forwarded verbatim to the underlying web input.
39
50
  "aria-controls"?: string;
@@ -45,6 +56,7 @@ export function TextInputField(props: TextInputFieldProps) {
45
56
  const locale = useLoticsLocale();
46
57
  const {
47
58
  style,
59
+ seamless,
48
60
  icon,
49
61
  clearable,
50
62
  onClear,
@@ -167,7 +179,8 @@ export function TextInputField(props: TextInputFieldProps) {
167
179
  showShortcut && shortcutWidth > 0 && { paddingRight: SHORTCUT_INSET + shortcutWidth },
168
180
  hovered && editable && { borderColor: HOVER_BORDER },
169
181
  style,
170
- focusVisible && { boxShadow: FOCUS_RING },
182
+ focusVisible && !seamless && { boxShadow: FOCUS_RING },
183
+ seamless && styles.seamless,
171
184
  ]}
172
185
  multiline={(numberOfLines && numberOfLines > 1) || multiline}
173
186
  focusable={editable}
@@ -227,6 +240,11 @@ const styles = StyleSheet.create({
227
240
  fontFamily: fontFamilyRegular,
228
241
  letterSpacing: -0.4,
229
242
  },
243
+ // The HOST paints the surface AND owns the text inset. Height and vertical
244
+ // padding stay (they hold the field's size); the horizontal padding goes,
245
+ // because the host already spent CONTROL_TEXT_INSET and two insets stacked
246
+ // shift the text 8px right the moment the field focuses.
247
+ seamless: { borderColor: "transparent", backgroundColor: "transparent", paddingHorizontal: 0 },
230
248
  disabled: {
231
249
  color: colors.zinc["400"],
232
250
  outlineStyle: "none" as unknown as "solid",
package/src/text_utils.ts CHANGED
@@ -26,7 +26,7 @@ export function getTextColor(color?: TextColor): string {
26
26
  case "zinc-500":
27
27
  return colors.zinc["500"];
28
28
  // The disabled ink — light enough that a control reads inert at a glance
29
- // (muted/600 still reads as body ink on a zinc-50 chip).
29
+ // (muted/600 still reads as body ink on the control's resting surface).
30
30
  case "zinc-400":
31
31
  return colors.zinc["400"];
32
32
  case "inverted":
@@ -1,157 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { PressableRow } from "./pressable_row";
4
- import { PressDoor } from "./press_door";
5
- import { Text } from "./text";
6
- import { Icon, type IconName } from "./icon";
7
- import { colors } from "./colors";
8
- import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
9
-
10
- /** One reference fact. `V` is the value the variant admits — text under a door, any node without one. */
11
- interface LinkedRecordFact<V = ReactNode> {
12
- label: string;
13
- value: V;
14
- }
15
-
16
- interface LinkedRecordBoxBaseProps {
17
- /** The linked record's kind glyph (building-2 for a company, file-text for a record…). */
18
- icon: IconName;
19
- /** The linked record's display name. */
20
- name: string;
21
- /** A secondary identity line — a code, tax id, country, or "<desk> record". */
22
- subtitle?: string;
23
- /** Width of the facts' label column (default 80). Widen for longer labels. */
24
- factLabelWidth?: number;
25
- /** The verbs, in a hairline-fenced footer at the bottom of the box, lifted above the door's
26
- * hit area. Convention: destructive LEFT, go-to RIGHT (a `<View style={{ flex: 1 }} />`
27
- * spacer between). The divider is drawn automatically when actions are present. Supplied by
28
- * the consumer so the labels stay localized and the verb set fits the link. */
29
- actions?: ReactNode;
30
- }
31
-
32
- /** The box that OPENS — the whole surface is the door into the linked record's own detail. */
33
- interface LinkedRecordBoxDoorProps extends LinkedRecordBoxBaseProps {
34
- /** Opens the linked record's detail (drawer/page) — the WHOLE box presses this. */
35
- onOpen: () => void;
36
- /** Accessible name for opening the record (e.g. "Acme Corp — details"). */
37
- doorLabel: string;
38
- /** Reference facts, stacked VERTICALLY (legible at any width, never squeezed). TEXT only
39
- * (`""` renders "—"): the box is one press target, so the only controls it may hold are the
40
- * `actions` verbs, which the footer fences off and lifts above the door. A fact the reader
41
- * must EDIT belongs on the linked record's own page — that is what the door is for. */
42
- facts?: LinkedRecordFact<string>[];
43
- }
44
-
45
- /** The box with nothing to open — the linked record has no detail surface of its own. */
46
- interface LinkedRecordBoxStaticProps extends LinkedRecordBoxBaseProps {
47
- /** Absent by construction. Typed `never` rather than left off so `doorLabel` can never arrive
48
- * without a handler — a named door that goes nowhere. */
49
- onOpen?: never;
50
- doorLabel?: never;
51
- /** Reference facts, stacked VERTICALLY. A string renders as text (`""` → "—"); a NODE renders
52
- * as given (no placeholder), so a fact can carry its own inline editor — safe here precisely
53
- * because nothing about this box presses. */
54
- facts?: LinkedRecordFact[];
55
- }
56
-
57
- type LinkedRecordBoxProps = LinkedRecordBoxDoorProps | LinkedRecordBoxStaticProps;
58
-
59
- /**
60
- * A LINKED RECORD shown for reference — a bordered box scoping ANOTHER record's data
61
- * (identity glyph · name · facts). Use it wherever one record points at another (a shipment's
62
- * customer, an invoice's party, a case's sibling); the picker / empty-state that REASSIGNS the
63
- * link is the consumer's, shown in place of the box.
64
- *
65
- * `onOpen` picks the variant:
66
- *
67
- * - **With `onOpen`** (+ `doorLabel`) the WHOLE box is a keyboard-accessible door into the
68
- * record's detail. Its facts are TEXT, and the `actions` footer is the one place a control
69
- * may live — everything else is press-through to the door.
70
- * - **Without it** the box is a STATIC reference card: no door, no tab stop, no pointer, nothing
71
- * announced as a button; the only interactive parts are `actions` and whatever a fact node
72
- * holds. This is the shape for a record with NO page of its own — there is nothing to open, so
73
- * the box IS where its values are read and edited, and an inline editor in a fact is honest.
74
- *
75
- * The a11y contract the door variant enforces (the error-prone part, hence a primitive): a
76
- * container with interactive descendants must NEVER be `role="button"` (invalid HTML). So the
77
- * box is a role-less `PressableRow`, a `PressDoor` sibling carries the tab stop / accessible
78
- * name / focus ring, and the interior verbs lift above it via `zIndex`.
79
- */
80
- export function LinkedRecordBox(props: LinkedRecordBoxProps) {
81
- const { icon, name, subtitle, factLabelWidth = 80, actions } = props;
82
- // A door box's facts are strings, and a string IS a ReactNode — so both variants render
83
- // through one path typed at the wider value.
84
- const facts: LinkedRecordFact[] = props.facts ?? [];
85
- const body = (
86
- <>
87
- <View style={styles.identity}>
88
- <View style={styles.glyph}>
89
- <Icon name={icon} size={17} color={colors.zinc[600]} />
90
- </View>
91
- <View style={{ flex: 1, minWidth: 0 }}>
92
- <Text size="sm" weight="medium" numberOfLines={1}>{name}</Text>
93
- {subtitle ? <Text size="xs" color="muted" numberOfLines={1}>{subtitle}</Text> : null}
94
- </View>
95
- </View>
96
- {facts.length > 0 ? (
97
- <View style={{ gap: 6 }}>
98
- {facts.map((f) => {
99
- // THE ALIGNMENT LAW, the same one `DetailRow` runs on: the fact TOP-aligns and each
100
- // cell centres within the first CONTROL LINE. A node value brings the kit's control
101
- // band (an inline editor is 40 tall, a chip less), so both cells claim it — without
102
- // it the label `Text`, stretched to the row, printed its words at the band's TOP,
103
- // a half-band above the value it names. A STRING value has no band: label and value
104
- // are the same 20px line, so they sit at the row's top and a long value that wraps
105
- // still tops out level with its label.
106
- const banded = typeof f.value !== "string";
107
- return (
108
- <View key={f.label} style={styles.fact}>
109
- <View style={[{ width: factLabelWidth }, banded && styles.bandedCell]}>
110
- <Text size="sm" color="muted">{f.label}</Text>
111
- </View>
112
- {banded ? (
113
- // A node prints as authored — the "—" placeholder is a TEXT affordance, and a
114
- // control renders its own empty state.
115
- <View style={[{ flex: 1, minWidth: 0 }, styles.bandedCell]}>{f.value}</View>
116
- ) : (
117
- <Text size="sm" style={{ flex: 1 }}>{f.value || "—"}</Text>
118
- )}
119
- </View>
120
- );
121
- })}
122
- </View>
123
- ) : null}
124
- {actions ? (
125
- <>
126
- {/* full-bleed hairline fencing the verb footer off from the facts */}
127
- <View style={styles.divider} />
128
- <View style={styles.actions}>{actions}</View>
129
- </>
130
- ) : null}
131
- </>
132
- );
133
-
134
- // Nothing to open → a plain View, so the box takes no tab stop, announces no role, and paints
135
- // no pointer/hover promise of a destination it doesn't have.
136
- if (!props.onOpen) return <View style={styles.box}>{body}</View>;
137
-
138
- return (
139
- <PressableRow onPress={props.onOpen} style={styles.box}>
140
- <PressDoor accessibilityLabel={props.doorLabel} onPress={props.onOpen} />
141
- {body}
142
- </PressableRow>
143
- );
144
- }
145
-
146
- const styles = StyleSheet.create({
147
- box: { flexDirection: "column", alignItems: "stretch", borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 10, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 10, gap: 12 },
148
- identity: { flexDirection: "row", alignItems: "center", gap: 10 },
149
- glyph: { width: 34, height: 34, borderRadius: 8, backgroundColor: colors.zinc[100], alignItems: "center", justifyContent: "center" },
150
- fact: { flexDirection: "row", gap: 10, alignItems: "flex-start" },
151
- // A cell on the control band: exactly `minHeight` tall for a one-line label / a short chip,
152
- // and taller content (a wrapped label, a stack of controls) tops out level instead of being
153
- // dragged toward the block's centre.
154
- bandedCell: { minHeight: INLINE_CONTROL_HEIGHT, justifyContent: "center" },
155
- divider: { height: 1, backgroundColor: colors.zinc[100], marginHorizontal: -16 },
156
- actions: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", columnGap: 8, rowGap: 8, zIndex: 1 },
157
- });