@lotics/ui 28.3.0 → 29.2.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 +12 -1
- package/MIGRATION.md +26 -0
- package/docs/ai_patterns.md +158 -111
- package/docs/catalog.md +93 -24
- package/docs/templates.md +31 -12
- package/examples/tpl_item_list.tsx +201 -28
- package/examples/tpl_record.tsx +390 -123
- package/examples/tpl_task_board.tsx +45 -29
- package/package.json +6 -3
- package/src/agent_run.tsx +1 -1
- package/src/chip_group.tsx +10 -1
- package/src/clarify_wizard.tsx +1 -1
- package/src/diff_mark.tsx +110 -0
- package/src/diff_value.tsx +232 -0
- package/src/file_badge.tsx +2 -0
- package/src/file_row.tsx +58 -6
- package/src/file_thumbnail.tsx +113 -30
- package/src/file_thumbnail_grid.tsx +13 -1
- package/src/filter_chip.tsx +8 -2
- package/src/finding.tsx +2 -3
- package/src/inline_slot.tsx +58 -0
- package/src/inline_static.tsx +5 -23
- package/src/locale.tsx +18 -46
- package/src/result_header.tsx +1 -1
- package/src/use_change_set.ts +115 -0
- package/src/change_review.tsx +0 -800
package/src/file_row.tsx
CHANGED
|
@@ -9,8 +9,18 @@ import { useFocusRing } from "./use_focus_ring";
|
|
|
9
9
|
export interface FileRowProps {
|
|
10
10
|
/** The file / document name — the primary line. */
|
|
11
11
|
name: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Secondary line: size, status, timestamp — or a NODE, when the second line
|
|
14
|
+
* is itself carrying a change.
|
|
15
|
+
*
|
|
16
|
+
* A document set under review needs it: a file being RECLASSIFIED is the same
|
|
17
|
+
* bytes with a different filing ("Other" becomes "Certificate of origin"),
|
|
18
|
+
* and a file being SUPERSEDED wants the scan it displaces struck beneath it.
|
|
19
|
+
* Both are `DiffValue`s, and a string could only have described them in prose.
|
|
20
|
+
* A node renders OUTSIDE the muted single-line `Text` — that wrapper would
|
|
21
|
+
* clip it and its colour would fight the diff's own.
|
|
22
|
+
*/
|
|
23
|
+
meta?: React.ReactNode;
|
|
14
24
|
/** File type → the FileBadge glyph + color. Omit (with `placeholder`) for an
|
|
15
25
|
* expected-but-not-yet-provided document. */
|
|
16
26
|
mimeType?: string;
|
|
@@ -25,6 +35,16 @@ export interface FileRowProps {
|
|
|
25
35
|
* control. An independently-interactive SIBLING: never swallowed by the row
|
|
26
36
|
* press (a button never nests in the door button). */
|
|
27
37
|
trailing?: React.ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Leading slot, BEFORE the badge — a `DiffMark` saying what happened to this
|
|
40
|
+
* document, a selection checkbox, a status dot.
|
|
41
|
+
*
|
|
42
|
+
* It sits outside the badge rather than on it because it must line up DOWN
|
|
43
|
+
* the list: a mark drawn over a 30px badge moves with the badge's size, and
|
|
44
|
+
* `sm` and `md` rows would stop agreeing. Give every row the same slot,
|
|
45
|
+
* filled or empty, or the names stop forming a column.
|
|
46
|
+
*/
|
|
47
|
+
leading?: React.ReactNode;
|
|
28
48
|
/**
|
|
29
49
|
* Row weight. `"sm"` (default) is the compact attachment line — many files
|
|
30
50
|
* scanned as a list. `"md"` is the DOCUMENT-DESK row: a taller badge and a
|
|
@@ -53,6 +73,7 @@ export function FileRow({
|
|
|
53
73
|
isTemplate,
|
|
54
74
|
onPress,
|
|
55
75
|
trailing,
|
|
76
|
+
leading,
|
|
56
77
|
size = "sm",
|
|
57
78
|
}: FileRowProps) {
|
|
58
79
|
const md = size === "md";
|
|
@@ -68,16 +89,22 @@ export function FileRow({
|
|
|
68
89
|
|
|
69
90
|
const content = (
|
|
70
91
|
<>
|
|
92
|
+
{leading}
|
|
71
93
|
<FileBadge size={md ? 38 : 30} mimeType={mimeType} placeholder={placeholder} isTemplate={isTemplate} />
|
|
72
94
|
<View style={styles.text}>
|
|
73
95
|
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
74
96
|
{name}
|
|
75
97
|
</Text>
|
|
76
|
-
{
|
|
98
|
+
{/* A STRING keeps the muted single-line treatment; a node renders as
|
|
99
|
+
itself. Wrapping a node here would clip it at one line and tint it
|
|
100
|
+
muted, which is exactly wrong for a diff that has its own colour. */}
|
|
101
|
+
{meta == null || meta === "" ? null : typeof meta === "string" ? (
|
|
77
102
|
<Text size="xs" color="muted" numberOfLines={1}>
|
|
78
103
|
{meta}
|
|
79
104
|
</Text>
|
|
80
|
-
) :
|
|
105
|
+
) : (
|
|
106
|
+
meta
|
|
107
|
+
)}
|
|
81
108
|
</View>
|
|
82
109
|
</>
|
|
83
110
|
);
|
|
@@ -117,19 +144,44 @@ export function FileRow({
|
|
|
117
144
|
);
|
|
118
145
|
}
|
|
119
146
|
|
|
147
|
+
/**
|
|
148
|
+
* THE ROW BEAT, owned in one place.
|
|
149
|
+
*
|
|
150
|
+
* `sm` used to have no minimum and no vertical padding at all, so a static row
|
|
151
|
+
* was as tall as its own contents — 37px measured — while the same component
|
|
152
|
+
* with an `onPress` came out at 49, because only the pressable variant carried
|
|
153
|
+
* padding for its hover wash. Whether a row was a DOOR decided how tall it was,
|
|
154
|
+
* which is two owners for one property that happened never to be compared.
|
|
155
|
+
*
|
|
156
|
+
* 37 was also under every other row beat in the kit — `DetailRow` 40, the
|
|
157
|
+
* `TableRow` 52, this component's own `md` 56 — which is what "squeezed"
|
|
158
|
+
* literally was: a file line packed tighter than any list it sits beside.
|
|
159
|
+
*/
|
|
160
|
+
const ROW_MIN_HEIGHT = 48;
|
|
161
|
+
const ROW_PAD_Y = 6;
|
|
162
|
+
|
|
120
163
|
const styles = StyleSheet.create({
|
|
121
|
-
row: {
|
|
164
|
+
row: {
|
|
165
|
+
flexDirection: "row",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
gap: 12,
|
|
168
|
+
minHeight: ROW_MIN_HEIGHT,
|
|
169
|
+
// The pressable variant's padding too, so the badge sits at the same height
|
|
170
|
+
// in both and a list mixing them keeps one baseline.
|
|
171
|
+
paddingVertical: ROW_PAD_Y,
|
|
172
|
+
},
|
|
122
173
|
pressableRow: {
|
|
123
174
|
flexDirection: "row",
|
|
124
175
|
alignItems: "center",
|
|
125
176
|
gap: 12,
|
|
177
|
+
minHeight: ROW_MIN_HEIGHT,
|
|
126
178
|
// Negative horizontal margin cancels the padding for the CONTENT's position
|
|
127
179
|
// (badge stays aligned with static rows / the container edge) while the hover
|
|
128
180
|
// wash bleeds 8px outward into the parent's padding — a hit area with no
|
|
129
181
|
// layout shift. The parent is expected to have ≥8px horizontal padding.
|
|
130
182
|
marginHorizontal: -8,
|
|
131
183
|
paddingHorizontal: 8,
|
|
132
|
-
paddingVertical:
|
|
184
|
+
paddingVertical: ROW_PAD_Y,
|
|
133
185
|
borderRadius: 8,
|
|
134
186
|
...({ transitionDuration: "0.1s", transitionProperty: "background-color" } as object),
|
|
135
187
|
},
|
package/src/file_thumbnail.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
Linking,
|
|
22
22
|
} from "react-native";
|
|
23
23
|
import { isImageMimeType, isVideoMimeType, isAudioMimeType } from "./mime";
|
|
24
|
+
import { DiffMark, type DiffKind } from "./diff_mark";
|
|
24
25
|
|
|
25
26
|
export const THUMBNAIL_SIZE = 96;
|
|
26
27
|
export const COMPACT_THUMBNAIL_SIZE = 32;
|
|
@@ -86,14 +87,67 @@ interface FileThumbnailProps {
|
|
|
86
87
|
/** Accessible name for the tile — what pressing it DOES, when the raw filename isn't
|
|
87
88
|
* the useful name ("Xem chứng từ"). Defaults to `file.filename`. */
|
|
88
89
|
accessibilityLabel?: string;
|
|
90
|
+
/**
|
|
91
|
+
* WHAT A PROPOSAL DOES TO THIS TILE — a corner mark for a document set under
|
|
92
|
+
* review, rendered as a grid rather than as a list.
|
|
93
|
+
*
|
|
94
|
+
* Top-LEFT because it is the one corner nothing else claims (remove sits
|
|
95
|
+
* top-right, selection bottom-right, the uploading scrim covers everything),
|
|
96
|
+
* and because it puts the mark where the row list already puts it.
|
|
97
|
+
*
|
|
98
|
+
* The disc rides a white RING: a tile's ground is an arbitrary photo, and a
|
|
99
|
+
* pale tint over a pale scan is not a mark, it is a smudge. `removed`
|
|
100
|
+
* additionally fades the tile BODY — the grid's way of saying a document is
|
|
101
|
+
* on its way off the record — while the mark itself stays at full strength,
|
|
102
|
+
* since a faded mark is the exact smudge the ring exists to prevent.
|
|
103
|
+
*
|
|
104
|
+
* Ignored on a COMPACT tile (≤ 32px): a 22px disc on a 32px tile is not a
|
|
105
|
+
* corner mark, it is the tile. A compact strip lives inside a row, and that
|
|
106
|
+
* row is where its change belongs.
|
|
107
|
+
*
|
|
108
|
+
* A grid expresses MEMBERSHIP (this document is arriving, that one is going)
|
|
109
|
+
* and expresses PAIRING badly: a 96px tile has nowhere to say which file a
|
|
110
|
+
* replacement supersedes. Use `FileRow` for a set where things are being
|
|
111
|
+
* replaced rather than added and removed.
|
|
112
|
+
*/
|
|
113
|
+
diff?: DiffKind;
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The tile's diff mark — the same disc the rows use, on a white RING.
|
|
118
|
+
*
|
|
119
|
+
* The ring rather than the disc alone because a tile's ground is an arbitrary
|
|
120
|
+
* photo, and a pale tint over a pale scan is a smudge. It is circular rather
|
|
121
|
+
* than a rounded square: two shapes for one mark is exactly the inconsistency
|
|
122
|
+
* the disc exists to end.
|
|
123
|
+
*/
|
|
124
|
+
function DiffCorner({ kind }: { kind: DiffKind }) {
|
|
125
|
+
if (kind === "unchanged") return null;
|
|
126
|
+
return (
|
|
127
|
+
<View style={diffCornerStyles.chip} pointerEvents="none">
|
|
128
|
+
<DiffMark kind={kind} />
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const diffCornerStyles = StyleSheet.create({
|
|
134
|
+
chip: {
|
|
135
|
+
position: "absolute",
|
|
136
|
+
top: 4,
|
|
137
|
+
left: 4,
|
|
138
|
+
zIndex: 2,
|
|
139
|
+
backgroundColor: colors.white,
|
|
140
|
+
borderRadius: 999,
|
|
141
|
+
padding: 2,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
91
145
|
/**
|
|
92
146
|
* Displays a file as a square thumbnail.
|
|
93
147
|
* Works with any file source via DisplayFile interface.
|
|
94
148
|
*/
|
|
95
149
|
export function FileThumbnail(props: FileThumbnailProps) {
|
|
96
|
-
const { file, size, onPress, onLongPress, onRemove, selected, uploading, isTemplate, disablePress } = props;
|
|
150
|
+
const { file, size, onPress, onLongPress, onRemove, selected, uploading, isTemplate, disablePress, diff } = props;
|
|
97
151
|
const accessibilityLabel = props.accessibilityLabel ?? file.filename;
|
|
98
152
|
// A tile with no `onPress` still OPENS the file — a useful default for a bare
|
|
99
153
|
// thumbnail in a list or a gallery. But a caller that said `disablePress`
|
|
@@ -106,13 +160,34 @@ export function FileThumbnail(props: FileThumbnailProps) {
|
|
|
106
160
|
const rootStyle =
|
|
107
161
|
size !== undefined ? { width: size, height: size } : { width: "100%" as const, aspectRatio: 1 };
|
|
108
162
|
|
|
163
|
+
// A tile ≤ 32px is a badge in a strip, not a card: a 22px mark would swallow
|
|
164
|
+
// it. The row or cell holding the strip carries the change instead.
|
|
165
|
+
const compact = size !== undefined && size <= COMPACT_THUMBNAIL_SIZE;
|
|
166
|
+
const mark = diff === undefined || compact ? null : <DiffCorner kind={diff} />;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A document on its way OFF the record reads as leaving, and the fade says
|
|
170
|
+
* that across the whole tile — which is what a grid is scanned by.
|
|
171
|
+
*
|
|
172
|
+
* It wraps the BODY rather than the root on purpose. `opacity` cascades to
|
|
173
|
+
* the entire subtree, so on the root it faded the corner mark too, at exactly
|
|
174
|
+
* the moment the mark has the most to say. That is also the dimming
|
|
175
|
+
* `DiffMark`'s ink was darkened for; putting the mark inside it would have
|
|
176
|
+
* spent that margin for nothing.
|
|
177
|
+
*/
|
|
178
|
+
const body = (node: React.ReactNode) =>
|
|
179
|
+
diff === "removed" ? <View style={styles.fadedBody}>{node}</View> : node;
|
|
180
|
+
|
|
109
181
|
if (isImageMimeType(file.mimeType)) {
|
|
110
182
|
return (
|
|
111
183
|
<View style={rootStyle}>
|
|
112
|
-
|
|
184
|
+
{body(
|
|
185
|
+
<ImageThumbnail file={file} size={size} accessibilityLabel={accessibilityLabel} onPress={press} onLongPress={onLongPress} />,
|
|
186
|
+
)}
|
|
113
187
|
{uploading && <UploadingOverlay size={size} />}
|
|
114
188
|
{onRemove && <RemoveButton onPress={onRemove} />}
|
|
115
189
|
{selected !== undefined && <SelectionOverlay selected={selected} />}
|
|
190
|
+
{mark}
|
|
116
191
|
</View>
|
|
117
192
|
);
|
|
118
193
|
}
|
|
@@ -122,46 +197,52 @@ export function FileThumbnail(props: FileThumbnailProps) {
|
|
|
122
197
|
if (size !== undefined && size <= COMPACT_THUMBNAIL_SIZE && mediaIcon === undefined) {
|
|
123
198
|
return (
|
|
124
199
|
<View style={rootStyle}>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
200
|
+
{body(
|
|
201
|
+
<DocumentBadge
|
|
202
|
+
mimeType={file.mimeType}
|
|
203
|
+
size={size}
|
|
204
|
+
isTemplate={isTemplate}
|
|
205
|
+
accessibilityLabel={accessibilityLabel}
|
|
206
|
+
onPress={pressOrOpen}
|
|
207
|
+
onLongPress={onLongPress}
|
|
208
|
+
/>,
|
|
209
|
+
)}
|
|
133
210
|
{uploading && <UploadingOverlay size={size} />}
|
|
134
211
|
{onRemove && <RemoveButton onPress={onRemove} />}
|
|
212
|
+
{mark}
|
|
135
213
|
</View>
|
|
136
214
|
);
|
|
137
215
|
}
|
|
138
216
|
|
|
139
217
|
return (
|
|
140
218
|
<View style={rootStyle}>
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
219
|
+
{body(
|
|
220
|
+
mediaIcon !== undefined ? (
|
|
221
|
+
<MediaCard
|
|
222
|
+
mimeType={file.mimeType}
|
|
223
|
+
filename={file.filename}
|
|
224
|
+
icon={mediaIcon}
|
|
225
|
+
size={size}
|
|
226
|
+
accessibilityLabel={accessibilityLabel}
|
|
227
|
+
onPress={pressOrOpen}
|
|
228
|
+
onLongPress={onLongPress}
|
|
229
|
+
/>
|
|
230
|
+
) : (
|
|
231
|
+
<DocumentCard
|
|
232
|
+
mimeType={file.mimeType}
|
|
233
|
+
filename={file.filename}
|
|
234
|
+
size={size}
|
|
235
|
+
isTemplate={isTemplate}
|
|
236
|
+
accessibilityLabel={accessibilityLabel}
|
|
237
|
+
onPress={pressOrOpen}
|
|
238
|
+
onLongPress={onLongPress}
|
|
239
|
+
/>
|
|
240
|
+
),
|
|
161
241
|
)}
|
|
162
242
|
{uploading && <UploadingOverlay size={size} />}
|
|
163
243
|
{onRemove && <RemoveButton onPress={onRemove} />}
|
|
164
244
|
{selected !== undefined && <SelectionOverlay selected={selected} />}
|
|
245
|
+
{mark}
|
|
165
246
|
</View>
|
|
166
247
|
);
|
|
167
248
|
}
|
|
@@ -456,6 +537,8 @@ function SelectionOverlay({ selected }: { selected: boolean }) {
|
|
|
456
537
|
// =============================================================================
|
|
457
538
|
|
|
458
539
|
const styles = StyleSheet.create({
|
|
540
|
+
// Fills the tile so the fade covers the whole body and nothing reflows.
|
|
541
|
+
fadedBody: { width: "100%", height: "100%", opacity: 0.45 },
|
|
459
542
|
documentCard: {
|
|
460
543
|
borderRadius: 10,
|
|
461
544
|
backgroundColor: colors.white,
|
|
@@ -15,6 +15,7 @@ import { View, Pressable, StyleSheet, type ViewStyle } from "react-native";
|
|
|
15
15
|
import { Text } from "./text";
|
|
16
16
|
import { colors } from "./colors";
|
|
17
17
|
import { FileThumbnail, type DisplayFile, THUMBNAIL_SIZE, COMPACT_THUMBNAIL_SIZE } from "./file_thumbnail";
|
|
18
|
+
import { type DiffKind } from "./diff_mark";
|
|
18
19
|
import { FOCUS_RING } from "./control_surface";
|
|
19
20
|
import { useFocusRing } from "./use_focus_ring";
|
|
20
21
|
|
|
@@ -171,12 +172,22 @@ export interface FileThumbnailGridProps {
|
|
|
171
172
|
disablePress?: boolean;
|
|
172
173
|
/** Selected file IDs — renders a selection overlay on those thumbnails. */
|
|
173
174
|
selectedIds?: ReadonlySet<string>;
|
|
175
|
+
/**
|
|
176
|
+
* What a proposal does to each file, keyed by id — a corner mark on the tile
|
|
177
|
+
* and a fade on anything being removed. Keyed the same way `selectedIds` is,
|
|
178
|
+
* so a grid under review is the SAME grid with one more map passed in.
|
|
179
|
+
*
|
|
180
|
+
* A grid says MEMBERSHIP well and PAIRING badly: there is nowhere on a 96px
|
|
181
|
+
* tile to name the file a replacement supersedes. A set where documents are
|
|
182
|
+
* being replaced rather than added and removed wants `FileRow`.
|
|
183
|
+
*/
|
|
184
|
+
diffs?: ReadonlyMap<string, DiffKind>;
|
|
174
185
|
onFilePress?: (file: DisplayFile) => void;
|
|
175
186
|
onRemove?: (id: string) => void;
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
export function FileThumbnailGrid(props: FileThumbnailGridProps) {
|
|
179
|
-
const { files, selectedIds, onFilePress, onRemove, disablePress, ...layout } = props;
|
|
190
|
+
const { files, selectedIds, diffs, onFilePress, onRemove, disablePress, ...layout } = props;
|
|
180
191
|
return (
|
|
181
192
|
<ThumbnailGrid
|
|
182
193
|
{...layout}
|
|
@@ -187,6 +198,7 @@ export function FileThumbnailGrid(props: FileThumbnailGridProps) {
|
|
|
187
198
|
<FileThumbnail
|
|
188
199
|
file={file}
|
|
189
200
|
size={size}
|
|
201
|
+
diff={diffs?.get(file.id)}
|
|
190
202
|
onPress={disablePress || !onFilePress ? undefined : () => onFilePress(file)}
|
|
191
203
|
// Withholding `onPress` is NOT enough: a tile with none falls back to
|
|
192
204
|
// opening the file, so it stays a button and a tab stop. The prop has
|
package/src/filter_chip.tsx
CHANGED
|
@@ -93,11 +93,17 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
93
93
|
<Chip onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
|
|
94
94
|
{active && typeof summary !== "string" ? (
|
|
95
95
|
<View style={styles.summaryRow}>
|
|
96
|
-
<Text size="sm" weight="medium" color="zinc-700">{`${label}:`}</Text>
|
|
96
|
+
<Text userSelect="none" size="sm" weight="medium" color="zinc-700">{`${label}:`}</Text>
|
|
97
97
|
{summary}
|
|
98
98
|
</View>
|
|
99
99
|
) : (
|
|
100
|
-
<Text
|
|
100
|
+
<Text
|
|
101
|
+
userSelect="none"
|
|
102
|
+
size="sm"
|
|
103
|
+
weight="medium"
|
|
104
|
+
color={active ? "zinc-900" : "zinc-700"}
|
|
105
|
+
numberOfLines={1}
|
|
106
|
+
>
|
|
101
107
|
{active ? `${label}: ${summary}` : label}
|
|
102
108
|
</Text>
|
|
103
109
|
)}
|
package/src/finding.tsx
CHANGED
|
@@ -47,9 +47,8 @@ export interface FindingProps {
|
|
|
47
47
|
* observation, a briefing item. Good defaults (severity word, title, detail,
|
|
48
48
|
* the PROMINENT metric, `Sources` chips) with a `children` slot for any
|
|
49
49
|
* extra composition. Display-only: a finding informs the verdict the host
|
|
50
|
-
* records; it decides nothing itself. Stack several most-severe first
|
|
51
|
-
*
|
|
52
|
-
* dividers apply); standalone, separate them yourself.
|
|
50
|
+
* records; it decides nothing itself. Stack several most-severe first,
|
|
51
|
+
* separated by a `Divider`.
|
|
53
52
|
*/
|
|
54
53
|
export function Finding(props: FindingProps) {
|
|
55
54
|
const words = useLoticsLocale().finding;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
4
|
+
import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
5
|
+
|
|
6
|
+
export interface InlineSlotProps {
|
|
7
|
+
/** Whatever sits in the value column — a `DiffValue`, a chip, a pair. */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ANY node placed on the INLINE-CONTROL GRID — the box an `Inline*` editor
|
|
14
|
+
* draws, minus the editor.
|
|
15
|
+
*
|
|
16
|
+
* A record's value column mixes editors with things that are not editors: a
|
|
17
|
+
* computed total, a synced value, a proposed change. The editors are 40px tall
|
|
18
|
+
* with 8px of horizontal padding inside a 1px border, and their text therefore
|
|
19
|
+
* sits 9px in from the cell edge and centred in a 40px band. Anything rendered
|
|
20
|
+
* raw beside them starts at the cell edge, 20px tall, at the top — so the value
|
|
21
|
+
* column quietly acquires TWO left edges and two baselines, and `DetailRow`'s
|
|
22
|
+
* label (which pads down to meet a control) agrees with one kind of row and not
|
|
23
|
+
* the other.
|
|
24
|
+
*
|
|
25
|
+
* That is not a cosmetic drift. The column is the only thing telling a reader
|
|
26
|
+
* these values belong to one record, and it stops reading as a column.
|
|
27
|
+
*
|
|
28
|
+
* `InlineStatic` is the STRING case of this and composes it. Reach for the slot
|
|
29
|
+
* directly when the value is a node — which a review surface always is, since a
|
|
30
|
+
* proposed value is a `DiffValue` sitting where its editor would be.
|
|
31
|
+
*
|
|
32
|
+
* NOT for a table cell: a `Table` sets its own row rhythm, and a 40px control
|
|
33
|
+
* band inside one only makes the rows taller.
|
|
34
|
+
*/
|
|
35
|
+
export function InlineSlot({ children, style }: InlineSlotProps) {
|
|
36
|
+
return <View style={[styles.box, style]}>{children}</View>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
// Matches `inline_edit`'s view box exactly (height, radius, padding, 1px
|
|
41
|
+
// transparent border) so the content's baseline aligns with the editors —
|
|
42
|
+
// minus the FocusRingPressable, so it never reads as an interactive control.
|
|
43
|
+
box: {
|
|
44
|
+
minHeight: INLINE_CONTROL_HEIGHT,
|
|
45
|
+
borderRadius: CONTROL_RADIUS,
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: "transparent",
|
|
48
|
+
paddingHorizontal: 8,
|
|
49
|
+
// The editors' vertical padding too, not just the horizontal. A single line
|
|
50
|
+
// hides its absence — the box is `minHeight` 40 and the text is ~20, so both
|
|
51
|
+
// sides land on 40 whether or not this exists — but an auto-growing input
|
|
52
|
+
// sizes itself to content PLUS its padding and border, so from the SECOND
|
|
53
|
+
// line on, a value without this sits ~14px shorter than the input that
|
|
54
|
+
// replaces it. Matching by coincidence at one line is not matching.
|
|
55
|
+
paddingVertical: 8,
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
},
|
|
58
|
+
});
|
package/src/inline_static.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { StyleSheet
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { inlineValueTextStyle } from "./inline_edit";
|
|
5
|
+
import { InlineSlot } from "./inline_slot";
|
|
6
6
|
|
|
7
7
|
export interface InlineStaticProps {
|
|
8
8
|
/** The value to show. Empty → `placeholder` (default "—"). */
|
|
@@ -45,7 +45,7 @@ export function InlineStatic(props: InlineStaticProps) {
|
|
|
45
45
|
const isEmpty = value.length === 0;
|
|
46
46
|
const display = isEmpty ? (placeholder ?? "—") : value;
|
|
47
47
|
return (
|
|
48
|
-
<
|
|
48
|
+
<InlineSlot>
|
|
49
49
|
<Text
|
|
50
50
|
numberOfLines={multiline ? undefined : 1}
|
|
51
51
|
tabular={tabular}
|
|
@@ -56,28 +56,10 @@ export function InlineStatic(props: InlineStaticProps) {
|
|
|
56
56
|
>
|
|
57
57
|
{display}
|
|
58
58
|
</Text>
|
|
59
|
-
</
|
|
59
|
+
</InlineSlot>
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const styles = StyleSheet.create({
|
|
64
|
-
// Matches `inline_edit`'s view box exactly (height, radius, padding, 1px
|
|
65
|
-
// transparent border) so the value's baseline aligns with the editors — minus
|
|
66
|
-
// the FocusRingPressable, so it never reads as an interactive control.
|
|
67
|
-
box: {
|
|
68
|
-
minHeight: INLINE_CONTROL_HEIGHT,
|
|
69
|
-
borderRadius: CONTROL_RADIUS,
|
|
70
|
-
borderWidth: 1,
|
|
71
|
-
borderColor: "transparent",
|
|
72
|
-
paddingHorizontal: 8,
|
|
73
|
-
// The editors' vertical padding too, not just the horizontal. A single line
|
|
74
|
-
// hides its absence — the box is `minHeight` 40 and the text is ~20, so both
|
|
75
|
-
// sides land on 40 whether or not this exists — but an auto-growing input
|
|
76
|
-
// sizes itself to content PLUS its padding and border, so from the SECOND
|
|
77
|
-
// line on, a value without this sits ~14px shorter than the input that
|
|
78
|
-
// replaces it. Matching by coincidence at one line is not matching.
|
|
79
|
-
paddingVertical: 8,
|
|
80
|
-
justifyContent: "center",
|
|
81
|
-
},
|
|
82
64
|
placeholder: { color: colors.zinc[400] },
|
|
83
65
|
});
|
package/src/locale.tsx
CHANGED
|
@@ -94,27 +94,17 @@ export interface LoticsLocale {
|
|
|
94
94
|
suggestionChip: { add: (label: string) => string; dismiss: string };
|
|
95
95
|
/** `Confidence`: the full level phrase ("High confidence" …). */
|
|
96
96
|
confidence: ConfidenceLabels;
|
|
97
|
-
/** `Finding`: the severity badge word ("Critical" …). */
|
|
98
97
|
/** `RemainderMeter`: the applied / remaining / over / exact captions. */
|
|
99
98
|
remainderMeter: Required<RemainderMeterLabels>;
|
|
100
|
-
/** `
|
|
101
|
-
* Keep/Drop verdicts, Undo, the Applied/Discarded tags, the kept-counter,
|
|
102
|
-
* the title, and the commit-bar Accept-all / Discard. */
|
|
99
|
+
/** `Finding`: the severity words + the comparison delta caption. */
|
|
103
100
|
finding: FindingLabels;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
discard: string;
|
|
112
|
-
apply: string;
|
|
113
|
-
recommended: string;
|
|
114
|
-
customValue: string;
|
|
115
|
-
opAdd: string;
|
|
116
|
-
opEdit: string;
|
|
117
|
-
opRemove: string;
|
|
101
|
+
/** `DiffMark`: what a proposal does to a row — the accessible word each mark
|
|
102
|
+
* announces, since the meaning may never rest on colour alone. */
|
|
103
|
+
diff: {
|
|
104
|
+
added: string;
|
|
105
|
+
changed: string;
|
|
106
|
+
removed: string;
|
|
107
|
+
unchanged: string;
|
|
118
108
|
};
|
|
119
109
|
/** `DateRangeFilterField` (and the `DateFilter` panel it wraps): presets,
|
|
120
110
|
* from/to, the footer Clear/Done, the trigger placeholder, and the time-field
|
|
@@ -273,20 +263,11 @@ export const en: LoticsLocale = {
|
|
|
273
263
|
exact: "Fully applied",
|
|
274
264
|
},
|
|
275
265
|
finding: { critical: "Critical", warning: "Warning", info: "Note", positive: "On track", difference: "Difference" },
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
keptCount: (kept, total) => `${kept} of ${total} kept`,
|
|
282
|
-
acceptAll: "Keep all",
|
|
283
|
-
discard: "Discard",
|
|
284
|
-
apply: "Apply",
|
|
285
|
-
recommended: "AI pick",
|
|
286
|
-
customValue: "Type another value",
|
|
287
|
-
opAdd: "Add",
|
|
288
|
-
opEdit: "Edit",
|
|
289
|
-
opRemove: "Delete",
|
|
266
|
+
diff: {
|
|
267
|
+
added: "Added",
|
|
268
|
+
changed: "Changed",
|
|
269
|
+
removed: "Removed",
|
|
270
|
+
unchanged: "Unchanged",
|
|
290
271
|
},
|
|
291
272
|
dateRange: {
|
|
292
273
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
@@ -437,20 +418,11 @@ export const vi: LoticsLocale = {
|
|
|
437
418
|
exact: "Đã phân bổ đủ",
|
|
438
419
|
},
|
|
439
420
|
finding: { critical: "Nghiêm trọng", warning: "Cảnh báo", info: "Ghi chú", positive: "Đúng tiến độ", difference: "Chênh lệch" },
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
keptCount: (kept, total) => `Đã giữ ${kept}/${total}`,
|
|
446
|
-
acceptAll: "Giữ tất cả",
|
|
447
|
-
discard: "Bỏ hết",
|
|
448
|
-
apply: "Áp dụng",
|
|
449
|
-
recommended: "AI đề xuất",
|
|
450
|
-
customValue: "Nhập giá trị khác",
|
|
451
|
-
opAdd: "Thêm",
|
|
452
|
-
opEdit: "Sửa",
|
|
453
|
-
opRemove: "Xóa",
|
|
421
|
+
diff: {
|
|
422
|
+
added: "Thêm mới",
|
|
423
|
+
changed: "Thay đổi",
|
|
424
|
+
removed: "Xóa",
|
|
425
|
+
unchanged: "Giữ nguyên",
|
|
454
426
|
},
|
|
455
427
|
dateRange: {
|
|
456
428
|
year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
|
package/src/result_header.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { Icon, type IconName } from "./icon";
|
|
|
5
5
|
import { Text, type HeadingLevel } from "./text";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Result header — the strip that opens the POST-SAVE RECEIPT.
|
|
8
|
+
* Result header — the strip that opens the POST-SAVE RECEIPT. A review surface gates a proposal
|
|
9
9
|
* BEFORE it applies; this header opens the receipt for what a save-direct run
|
|
10
10
|
* ALREADY CREATED. The receipt states the outcome and routes — it never edits
|
|
11
11
|
* (correction happens through the record's ordinary verbs, on the record):
|