@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.
- package/AGENTS.md +9 -2
- package/MIGRATION.md +142 -2
- package/docs/ai_patterns.md +22 -22
- package/docs/catalog.md +119 -98
- package/docs/composition.md +134 -42
- package/docs/data_entry.md +67 -31
- package/docs/templates.md +86 -68
- package/examples/tpl_allocate.tsx +5 -5
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +6 -6
- package/examples/tpl_dashboard.tsx +9 -9
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_item_list.tsx +64 -37
- package/examples/tpl_lookup.tsx +5 -5
- package/examples/tpl_pick.tsx +6 -6
- package/examples/tpl_pivot.tsx +3 -3
- package/examples/tpl_record.tsx +919 -611
- package/examples/tpl_report.tsx +2 -2
- package/examples/tpl_rollup.tsx +5 -5
- package/examples/tpl_shifts.tsx +5 -5
- package/examples/tpl_statements.tsx +7 -7
- package/examples/tpl_stock.tsx +2 -2
- package/examples/tpl_task_board.tsx +42 -26
- package/examples/tpl_tower.tsx +6 -6
- package/package.json +3 -2
- package/src/agent_run.tsx +4 -4
- package/src/breakdown.tsx +1 -1
- package/src/calendar/calendar_view.tsx +1 -1
- package/src/change_review.tsx +9 -8
- package/src/chip_group.tsx +12 -2
- package/src/choice_list.tsx +2 -2
- package/src/confidence.tsx +2 -2
- package/src/data_grid.tsx +1 -1
- package/src/detail_row.tsx +50 -58
- package/src/file_dropzone.tsx +1 -1
- package/src/file_gallery_modal.tsx +3 -3
- package/src/file_rows.tsx +1 -1
- package/src/finding.tsx +4 -4
- package/src/form_field.tsx +1 -1
- package/src/format_date.ts +2 -2
- package/src/heatmap.tsx +1 -1
- package/src/inline_button.tsx +84 -0
- package/src/inline_date_picker.tsx +17 -10
- package/src/inline_edit.tsx +298 -59
- package/src/inline_member_select.tsx +8 -3
- package/src/inline_number_input.tsx +11 -4
- package/src/inline_select.tsx +26 -13
- package/src/inline_text_input.tsx +12 -4
- package/src/inline_time_picker.tsx +10 -4
- package/src/ledger.tsx +2 -2
- package/src/locale.tsx +7 -3
- package/src/matrix.tsx +1 -1
- package/src/number_input.tsx +18 -7
- package/src/pipeline.tsx +1 -1
- package/src/popover.tsx +1 -1
- package/src/press_door.tsx +1 -1
- package/src/pressable_highlight.tsx +1 -1
- package/src/progress_bar.tsx +3 -3
- package/src/record_summary.tsx +2 -2
- package/src/result_header.tsx +2 -2
- package/src/sequence.tsx +170 -0
- package/src/share_or_download.ts +2 -2
- package/src/step_progress.tsx +7 -5
- package/src/stepper.tsx +1 -1
- package/src/task.tsx +6 -6
- package/src/text_input_field.tsx +19 -1
- package/src/text_utils.ts +1 -1
- package/src/linked_record_box.tsx +0 -157
package/src/text_input_field.tsx
CHANGED
|
@@ -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
|
|
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
|
-
});
|