@lotics/ui 22.2.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 +27 -25
- 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 +40 -25
- 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/chip_group.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
1
|
+
import { StyleSheet, View, type ViewStyle } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
4
|
import { chipSurfaceStyle } from "./control_surface";
|
|
@@ -62,7 +62,13 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
62
62
|
// The shared pill-surface contract (height/border/radius/white +
|
|
63
63
|
// hover/press wash + the selected box-shadow ring); the chip only
|
|
64
64
|
// adds its horizontal padding.
|
|
65
|
-
|
|
65
|
+
// The ARROW cursor, like every other control that SETS A VALUE. The
|
|
66
|
+
// pointer is reserved for acts that go somewhere or do something —
|
|
67
|
+
// a row that opens a record, a button. A pill here is the field's
|
|
68
|
+
// value, so it reads like the editors beside it, not like a button.
|
|
69
|
+
// (`PressableHighlight` defaults to a pointer; a caller's style lands
|
|
70
|
+
// after its own, so this is the override, not a fight.)
|
|
71
|
+
style={(state) => [chipSurfaceStyle(state, { selected: active }), styles.chip]}
|
|
66
72
|
>
|
|
67
73
|
<Text size="sm" weight={active ? "semibold" : "medium"} color={active ? "default" : "muted"}>
|
|
68
74
|
{option.label}
|
|
@@ -73,3 +79,7 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
73
79
|
</View>
|
|
74
80
|
);
|
|
75
81
|
}
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
chip: { paddingHorizontal: 14, ...({ cursor: "auto" } as ViewStyle) },
|
|
85
|
+
});
|
package/src/choice_list.tsx
CHANGED
|
@@ -74,8 +74,8 @@ function ChoiceRow({ option, selected, onSelect }: { option: ChoiceOption; selec
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/** The custom-answer row — the SAME box as a `ChoiceRow` (padding
|
|
78
|
-
* wash
|
|
77
|
+
/** The custom-answer row — the SAME box as a `ChoiceRow` (padding, radius, hover
|
|
78
|
+
* wash, focus ring), but holding an always-editable, auto-growing multiline input
|
|
79
79
|
* instead of a pressable label. Typing it IS the selection; focusing it re-selects a
|
|
80
80
|
* parked draft (`onFocus`). */
|
|
81
81
|
function CustomAnswerRow({ value, selected, onChangeText, onFocus, placeholder }: { value: string; selected: boolean; onChangeText: (text: string) => void; onFocus: () => void; placeholder: string }) {
|
package/src/confidence.tsx
CHANGED
|
@@ -14,7 +14,7 @@ export interface ConfidenceLabels {
|
|
|
14
14
|
low: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** A 0–1 score folded onto the three levels: >=0.8 high
|
|
17
|
+
/** A 0–1 score folded onto the three levels: >=0.8 high, >=0.5 medium, else low. */
|
|
18
18
|
export function levelFromScore(score: number): ConfidenceLevel {
|
|
19
19
|
if (score >= 0.8) return "high";
|
|
20
20
|
if (score >= 0.5) return "medium";
|
|
@@ -25,7 +25,7 @@ const FILLED: Record<ConfidenceLevel, number> = { high: 3, medium: 2, low: 1 };
|
|
|
25
25
|
const COLOR: Record<ConfidenceLevel, ColorName> = { high: "green", medium: "amber", low: "red" };
|
|
26
26
|
|
|
27
27
|
export interface ConfidenceProps {
|
|
28
|
-
/** Pass a level directly, or a 0–1 `score` (>=0.8 high
|
|
28
|
+
/** Pass a level directly, or a 0–1 `score` (>=0.8 high, >=0.5 medium, else low). */
|
|
29
29
|
level?: ConfidenceLevel;
|
|
30
30
|
score?: number;
|
|
31
31
|
labels?: Partial<ConfidenceLabels>;
|
package/src/data_grid.tsx
CHANGED
|
@@ -59,7 +59,7 @@ export interface DataGridProps<T> {
|
|
|
59
59
|
* high-volume paginated register (`Table` + `Pagination` + `Drawer`). It owns the
|
|
60
60
|
* sortable header, the collapsible grouped sections, and the column-aligned rows;
|
|
61
61
|
* the consumer owns the DATA, the sort/group/filter/collapse STATE, the toolbar
|
|
62
|
-
* (search
|
|
62
|
+
* (search, group-by, filters), the per-group add (`renderGroupFooter`), and every
|
|
63
63
|
* CELL (`columns[].cell` renders ANY field). It renders ALL rows — no pagination /
|
|
64
64
|
* virtualization — so it's for hundreds, low-thousands; at 10k+ use the register.
|
|
65
65
|
* Worked example: `tpl_task_board`.
|
package/src/detail_row.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createContext, ReactNode, useContext, useState } from "react";
|
|
2
2
|
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
3
3
|
import { MIN_CONTROL_WIDTH } from "./control_surface";
|
|
4
|
+
import { SPACE } from "./spacing";
|
|
4
5
|
import { FieldAnnotationProps, FieldAnnotations, hasFieldAnnotation } from "./field_annotations";
|
|
5
6
|
import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
6
7
|
import { getInputLineHeight } from "./text_utils";
|
|
@@ -8,7 +9,6 @@ import { Text } from "./text";
|
|
|
8
9
|
|
|
9
10
|
interface DetailTableColumns {
|
|
10
11
|
labelWidth: number;
|
|
11
|
-
trailingWidth?: number;
|
|
12
12
|
minHeight: number;
|
|
13
13
|
/** Container too narrow for the side-by-side columns — rows render STACKED
|
|
14
14
|
* (label above a full-width value row). */
|
|
@@ -28,11 +28,6 @@ export const DETAIL_LABEL_WIDTH = 130;
|
|
|
28
28
|
export interface DetailTableProps {
|
|
29
29
|
/** Label column width (px). Default 130. */
|
|
30
30
|
labelWidth?: number;
|
|
31
|
-
/** Reserved width (px) for the trailing column. Set it when ANY row carries a
|
|
32
|
-
* trailing action/unit/badge: EVERY row then reserves the column, so all
|
|
33
|
-
* value cells share ONE width and the trailing items align at one x — the
|
|
34
|
-
* table look. Omit when no row has a trailing (values fill to the edge). */
|
|
35
|
-
trailingWidth?: number;
|
|
36
31
|
/** Min row height. Default 40 (`INLINE_CONTROL_HEIGHT`) so editor, static,
|
|
37
32
|
* and custom rows all sit on the inline-control baseline grid. */
|
|
38
33
|
minHeight?: number;
|
|
@@ -47,35 +42,37 @@ export interface DetailTableProps {
|
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
/**
|
|
50
|
-
* The column grid for a stack of `DetailRow`s — label
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
45
|
+
* The column grid for a stack of `DetailRow`s — label, value laid out like a
|
|
46
|
+
* TABLE: one label width, one value width, set ONCE on the parent instead of
|
|
47
|
+
* repeated (and drifting) per row. The value column FILLS whatever the label
|
|
48
|
+
* leaves; there is no third column, and nothing a row does can make its
|
|
49
|
+
* neighbours' editors narrower.
|
|
55
50
|
*
|
|
56
|
-
* <DetailTable labelWidth={130}
|
|
57
|
-
* <DetailRow label="Reference"
|
|
58
|
-
* <InlineTextInput … />
|
|
51
|
+
* <DetailTable labelWidth={130}>
|
|
52
|
+
* <DetailRow label="Reference">
|
|
53
|
+
* <InlineTextInput … actions={<InlineButton title="Copy" … />} />
|
|
59
54
|
* </DetailRow>
|
|
60
55
|
* <DetailRow label="Priority"><InlineSelect … /></DetailRow>
|
|
61
56
|
* </DetailTable>
|
|
62
57
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
* A verb about a VALUE rides the FIELD (`InlineButton` in an editor's
|
|
59
|
+
* `actions`), never a column beside it: the act names the value, so it travels
|
|
60
|
+
* with what it acts on, and a reserved column is paid by every row for the
|
|
61
|
+
* benefit of one.
|
|
62
|
+
*
|
|
63
|
+
* It owns the row gap and the 40px row height of the inline-control grid.
|
|
65
64
|
*
|
|
66
65
|
* RESPONSIVE: the table measures its own container (not the viewport — a table
|
|
67
66
|
* inside a narrow Drawer on a wide screen must adapt too). When the columns
|
|
68
67
|
* would crush the value cell below a usable width, every row STACKS: label
|
|
69
|
-
* above a full-width value
|
|
70
|
-
* the switch is automatic.
|
|
68
|
+
* above a full-width value. No prop — the switch is automatic.
|
|
71
69
|
*/
|
|
72
70
|
export function DetailTable(props: DetailTableProps) {
|
|
73
|
-
const { labelWidth = DETAIL_LABEL_WIDTH,
|
|
71
|
+
const { labelWidth = DETAIL_LABEL_WIDTH, minHeight = INLINE_CONTROL_HEIGHT, minValueWidth = MIN_CONTROL_WIDTH, children, style } = props;
|
|
74
72
|
const [width, setWidth] = useState<number | null>(null);
|
|
75
|
-
const stacked =
|
|
76
|
-
width != null && width < labelWidth + (trailingWidth ?? 0) + minValueWidth + 24;
|
|
73
|
+
const stacked = width != null && width < labelWidth + minValueWidth + 24;
|
|
77
74
|
return (
|
|
78
|
-
<DetailTableContext.Provider value={{ labelWidth,
|
|
75
|
+
<DetailTableContext.Provider value={{ labelWidth, minHeight, stacked }}>
|
|
79
76
|
<View
|
|
80
77
|
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
|
|
81
78
|
// Measure-then-REVEAL: the unmeasured first frame renders invisible
|
|
@@ -94,13 +91,6 @@ export interface DetailRowProps extends FieldAnnotationProps {
|
|
|
94
91
|
label: ReactNode;
|
|
95
92
|
/** The value — any node (a `Text`, a `Badge`, a stack). */
|
|
96
93
|
children: ReactNode;
|
|
97
|
-
/** An optional right-side slot, rendered as the LAST child of the row (after
|
|
98
|
-
* `children`) — a small action `Button`, a status `Badge`, an `IconButton`.
|
|
99
|
-
* NOT for units: "kg" / "₫" belong IN the value (`InlineNumberInput format`).
|
|
100
|
-
* In form mode (`labelWidth` set) the value column already fills
|
|
101
|
-
* the row, so the trailing pins to the right edge automatically. Inside a
|
|
102
|
-
* `DetailTable` with `trailingWidth` it sits in the reserved trailing column. */
|
|
103
|
-
trailing?: ReactNode;
|
|
104
94
|
/** Fixed label-column width (px) to align labels in a left column — for a peek
|
|
105
95
|
* or form-like stack. Omit for a spread row: the label takes the slack and the
|
|
106
96
|
* value sits at the right edge (the default drawer-detail look). Inside a
|
|
@@ -125,17 +115,21 @@ export interface DetailRowProps extends FieldAnnotationProps {
|
|
|
125
115
|
* to the right edge); pass `labelWidth` to align labels in a fixed left column —
|
|
126
116
|
* the value column then FILLS the rest of the row, so a stack of inline editors
|
|
127
117
|
* all span the same width (none jumps wider on edit). For a stack of rows, wrap
|
|
128
|
-
* them in a `DetailTable`: the label
|
|
129
|
-
*
|
|
118
|
+
* them in a `DetailTable`: the label column width lives ONCE on the parent and
|
|
119
|
+
* every row aligns.
|
|
120
|
+
*
|
|
121
|
+
* There is no trailing slot. A verb about the VALUE goes on the field
|
|
122
|
+
* (`InlineButton` in an editor's `actions`); anything else — a status `Badge`, a
|
|
123
|
+
* unit — belongs IN the value, composed beside the control.
|
|
130
124
|
*/
|
|
131
125
|
export function DetailRow(props: DetailRowProps) {
|
|
132
126
|
const table = useContext(DetailTableContext);
|
|
133
|
-
const { label, children,
|
|
127
|
+
const { label, children, labelSize = "sm", description, warning, error, flat } = props;
|
|
134
128
|
const labelWidth = props.labelWidth ?? table?.labelWidth;
|
|
135
129
|
const minHeight = props.minHeight ?? table?.minHeight ?? 28;
|
|
136
130
|
const form = labelWidth != null;
|
|
137
131
|
// Annotations (description / warning / error) live UNDER THE VALUE — the row
|
|
138
|
-
// grows, so the label
|
|
132
|
+
// grows, so the label centers against the CONTROL LINE, not the block.
|
|
139
133
|
const annotated = hasFieldAnnotation(props);
|
|
140
134
|
// A FLAT value (no painted chip) centers its text in the control band,
|
|
141
135
|
// leaving invisible slack below it — tuck the annotations up by that slack
|
|
@@ -169,12 +163,11 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
169
163
|
{/* Wraps, like the horizontal label — a stacked row has the FULL width
|
|
170
164
|
to spend, so clipping here would be gratuitous. */}
|
|
171
165
|
<Text weight="medium">{label}</Text>
|
|
172
|
-
{/* stacked wears the FORM grammar exactly: label
|
|
173
|
-
control
|
|
166
|
+
{/* stacked wears the FORM grammar exactly: label, description,
|
|
167
|
+
control, warning, error (the `FormField` order) */}
|
|
174
168
|
{description != null ? <Text color="muted">{description}</Text> : null}
|
|
175
169
|
<View style={[styles.stackedValueRow, { minHeight }]}>
|
|
176
170
|
<View style={styles.value}>{children}</View>
|
|
177
|
-
{trailing != null ? trailing : null}
|
|
178
171
|
</View>
|
|
179
172
|
{warning != null ? (
|
|
180
173
|
<Text size="xs" color="warning" accessibilityRole="alert" aria-live="polite">
|
|
@@ -189,10 +182,10 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
189
182
|
</View>
|
|
190
183
|
);
|
|
191
184
|
}
|
|
192
|
-
// ONE alignment law for every row: the row TOP-ALIGNS and each cell (label
|
|
193
|
-
// control
|
|
194
|
-
//
|
|
195
|
-
//
|
|
185
|
+
// ONE alignment law for every row: the row TOP-ALIGNS and each cell (label,
|
|
186
|
+
// control) centers within the first CONTROL LINE (`minHeight`) — a tall value
|
|
187
|
+
// block (a multi-control stack, annotations) never drags the label toward the
|
|
188
|
+
// block's center.
|
|
196
189
|
return (
|
|
197
190
|
<View style={[styles.row, { minHeight }]}>
|
|
198
191
|
{labelCell(form ? { width: labelWidth } : styles.flexLabel)}
|
|
@@ -207,20 +200,24 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
207
200
|
{flatTuck !== 0 ? <View style={{ marginTop: flatTuck }}>{annotations}</View> : annotations}
|
|
208
201
|
</View>
|
|
209
202
|
)}
|
|
210
|
-
{table?.trailingWidth != null ? (
|
|
211
|
-
<View style={[styles.trailingCell, { width: table.trailingWidth, minHeight }]}>{trailing}</View>
|
|
212
|
-
) : trailing != null ? (
|
|
213
|
-
<View style={[styles.trailingCell, { minHeight }]}>{trailing}</View>
|
|
214
|
-
) : null}
|
|
215
203
|
</View>
|
|
216
204
|
);
|
|
217
205
|
}
|
|
218
206
|
|
|
219
207
|
const styles = StyleSheet.create({
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
|
|
208
|
+
// `SPACE.md` between rows. It was 8 while a resting field was a tint: two soft
|
|
209
|
+
// chips that close together still read as separate. A field now carries a
|
|
210
|
+
// BORDER, so at 8 the page shows a stack of hard edges 8px apart and the rows
|
|
211
|
+
// fuse into one block — the same gap, twice as loud. 16 is the next rung of the
|
|
212
|
+
// 8-grid (12 would read as developer-tuned), and it also settles a second
|
|
213
|
+
// question: an annotated row's `description` sits 2px under ITS value, so the
|
|
214
|
+
// gap to the NEXT row has to be unmistakably larger or the annotation looks
|
|
215
|
+
// like it belongs to whatever follows.
|
|
216
|
+
table: { gap: SPACE.md },
|
|
217
|
+
// Stacked (narrow) mode: a row is a label-over-value BLOCK, so the gap between
|
|
218
|
+
// rows must beat the gap inside one — `SPACE.lg`, the next rung up, on the same
|
|
219
|
+
// grid (it was 14, off-scale).
|
|
220
|
+
tableStacked: { gap: SPACE.lg },
|
|
224
221
|
unmeasured: { opacity: 0 },
|
|
225
222
|
stackedRow: { gap: 4 },
|
|
226
223
|
stackedValueRow: {
|
|
@@ -242,7 +239,7 @@ const styles = StyleSheet.create({
|
|
|
242
239
|
// `flex-start` — that drops every single-line label to the band's top edge.
|
|
243
240
|
labelCell: { justifyContent: "center" },
|
|
244
241
|
// The first line of the value cell — the control centers in it exactly like
|
|
245
|
-
// the label
|
|
242
|
+
// the label cell does.
|
|
246
243
|
controlLine: { justifyContent: "center" },
|
|
247
244
|
// The 2px between the control line and the annotation block beneath it — the
|
|
248
245
|
// same rhythm `FieldAnnotations` puts between its own lines.
|
|
@@ -250,13 +247,8 @@ const styles = StyleSheet.create({
|
|
|
250
247
|
// Form mode (labelWidth set): the value column FILLS the row so every inline
|
|
251
248
|
// editor spans the SAME width — and none jumps wider when it swaps to the
|
|
252
249
|
// (flex:1) edit control. A stretch column, so an InlineEditView / InlineStatic
|
|
253
|
-
// child fills it edge-to-edge.
|
|
254
|
-
// it
|
|
250
|
+
// child fills it edge-to-edge. Something that is NOT the value but belongs
|
|
251
|
+
// beside it — a status Badge — composes into a row INSIDE this cell, so it
|
|
252
|
+
// costs only the row that wants it.
|
|
255
253
|
value: { flex: 1 },
|
|
256
|
-
// The reserved trailing column: every row renders it (empty or not) so value
|
|
257
|
-
// cells share one width and trailing items align at one x, like a table.
|
|
258
|
-
trailingCell: {
|
|
259
|
-
flexDirection: "row",
|
|
260
|
-
alignItems: "center",
|
|
261
|
-
},
|
|
262
254
|
});
|
package/src/file_dropzone.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export interface FileDropzoneProps {
|
|
|
22
22
|
label?: string;
|
|
23
23
|
/** Secondary line. Defaults to the locale's `fileDropzone.hint` ("or click to
|
|
24
24
|
* browse" / "hoặc bấm để chọn"); override with an accepted-types line
|
|
25
|
-
* ("
|
|
25
|
+
* ("…, PDF, ảnh"). */
|
|
26
26
|
hint?: string;
|
|
27
27
|
/** Main line while a drag hovers the zone ("Thả để tải lên"). */
|
|
28
28
|
dropLabel?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// FileGalleryModal — full-screen file preview with a toolbar (filename
|
|
2
|
-
//
|
|
1
|
+
// FileGalleryModal — full-screen file preview with a toolbar (filename, counter,
|
|
2
|
+
// actions, close), prev/next nav, and ESC close. Pure primitive: takes
|
|
3
3
|
// DisplayFile[] only, no Lotics domain coupling. Renders every previewable type
|
|
4
4
|
// (image/PDF/video/audio/Word/Excel/CSV) via FilePreview, whose document engines
|
|
5
5
|
// (pdfjs-dist, @lotics/docx, @lotics/xlsx) are optional peer deps loaded lazily —
|
|
@@ -232,7 +232,7 @@ export function FileGalleryModal(props: FileGalleryModalProps) {
|
|
|
232
232
|
// Phone: actions collapse to a ⋯ menu.
|
|
233
233
|
<ActionMenu items={actionItems} accessibilityLabel={l.actions} />
|
|
234
234
|
) : (
|
|
235
|
-
// Wider screens: every action inline (download
|
|
235
|
+
// Wider screens: every action inline (download, open, remove).
|
|
236
236
|
// Rotate lives on the image overlay, not the toolbar.
|
|
237
237
|
<>
|
|
238
238
|
<Button
|
package/src/file_rows.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FileRows — the common "show a list of files" surface, batteries included: each
|
|
2
2
|
// file is a FileRow whose whole-row press opens the built-in full-screen
|
|
3
|
-
// FileGalleryModal, with a ⋯ menu (Download
|
|
3
|
+
// FileGalleryModal, with a ⋯ menu (Download, Open in new tab, Remove) on the
|
|
4
4
|
// trailing edge. Pure kit composition (FileRow + ActionMenu + FileGalleryModal) —
|
|
5
5
|
// reach for the pieces directly only when you need a different shape.
|
|
6
6
|
|
package/src/finding.tsx
CHANGED
|
@@ -44,8 +44,8 @@ export interface FindingProps {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* One ranked insight from an AI check — a cross-check discrepancy, an audit
|
|
47
|
-
* observation, a briefing item. Good defaults (severity word
|
|
48
|
-
*
|
|
47
|
+
* observation, a briefing item. Good defaults (severity word, title, detail,
|
|
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
50
|
* records; it decides nothing itself. Stack several most-severe first —
|
|
51
51
|
* inside a `ChangeReview` wrap each in a display-only `Change` (the family's
|
|
@@ -86,7 +86,7 @@ export function Finding(props: FindingProps) {
|
|
|
86
86
|
|
|
87
87
|
export interface FindingComparisonProps {
|
|
88
88
|
/** The disagreeing sides — each value with where it came from
|
|
89
|
-
* ("invoice.pdf"
|
|
89
|
+
* ("invoice.pdf", "480 pcs"). Two or more. */
|
|
90
90
|
values: { label: string; value: string }[];
|
|
91
91
|
/** The computed difference, EMPHASIZED ("−40 pcs"). */
|
|
92
92
|
delta?: string;
|
|
@@ -96,7 +96,7 @@ export interface FindingComparisonProps {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* The expected-vs-actual body of a `Finding` — each side a labeled row
|
|
99
|
-
* (source
|
|
99
|
+
* (source, value), a hairline, then the DELTA emphasized. Compose it as the
|
|
100
100
|
* finding's children; it replaces ad-hoc metric text for any
|
|
101
101
|
* one-value-disagrees insight (quantities, totals, dates).
|
|
102
102
|
*/
|
package/src/form_field.tsx
CHANGED
|
@@ -101,7 +101,7 @@ export function FormField(props: FormFieldProps & { children: React.ReactNode })
|
|
|
101
101
|
<Spacer size={8} />
|
|
102
102
|
{/*
|
|
103
103
|
Below the control, above the error — the three tones read
|
|
104
|
-
description
|
|
104
|
+
description, warning, error, ascending severity. `role="alert"`
|
|
105
105
|
+ `aria-live="polite"` announce it politely on appearance.
|
|
106
106
|
*/}
|
|
107
107
|
<Text nativeID={warningId} color="warning" accessibilityRole="alert" aria-live="polite">
|
package/src/format_date.ts
CHANGED
|
@@ -103,8 +103,8 @@ export function toISODate(value: Date | string | null | undefined): string {
|
|
|
103
103
|
/**
|
|
104
104
|
* THE date-value formatter — the date sibling of `formatMoney`. Accepts a `Date` OR an ISO string
|
|
105
105
|
* and returns a localized display string; defaults to the home market. `format` picks the date
|
|
106
|
-
* style: `date` → `22/05/2026
|
|
107
|
-
* `dayMonth` → `22 thg 5
|
|
106
|
+
* style: `date` → `22/05/2026`; `medium` → `22 thg 5, 2026`; `long` → `22 tháng 5, 2026`;
|
|
107
|
+
* `dayMonth` → `22 thg 5`, `monthYear` → `Tháng 5 2026`. **`time: true` prepends the 24h time to
|
|
108
108
|
* ANY style** — `14:30 22/05/2026`, `14:30 22 tháng 5, 2026` (time-first). `compact` drops the
|
|
109
109
|
* year on the numeric `date` style. Never hand-roll a date with `padStart` / `getMonth` / a raw
|
|
110
110
|
* `Intl.DateTimeFormat` for VALUE display — call this. (Component-internal chrome is exempt — see
|
package/src/heatmap.tsx
CHANGED
|
@@ -57,7 +57,7 @@ export function Heatmap(props: HeatmapProps) {
|
|
|
57
57
|
return (
|
|
58
58
|
<Cell
|
|
59
59
|
key={col.key}
|
|
60
|
-
label={`${row.label}
|
|
60
|
+
label={`${row.label}, ${col.label}: ${formatValue(value)}`}
|
|
61
61
|
background={value === 0 ? colors.zinc[100] : withAlpha(color, 0.15 + 0.85 * (value / max))}
|
|
62
62
|
selected={isSelected}
|
|
63
63
|
onPress={
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type GestureResponderEvent } from "react-native";
|
|
2
|
+
import { Text } from "./text";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
5
|
+
import { CONTROL_TRANSITION } from "./control_surface";
|
|
6
|
+
|
|
7
|
+
/** The height that fits INSIDE the 40px inline control band with 6px of air
|
|
8
|
+
* above and below — the same air a `DetailRow`'s label gets against its value. */
|
|
9
|
+
const INLINE_BUTTON_HEIGHT = 28;
|
|
10
|
+
|
|
11
|
+
interface InlineButtonProps {
|
|
12
|
+
/** The verb, as a word. An inline button is never icon-only — it sits on a
|
|
13
|
+
* crowded value where a bare glyph reads as decoration on the field. */
|
|
14
|
+
title: string;
|
|
15
|
+
onPress: () => void;
|
|
16
|
+
/** Announced name, when the surrounding value makes `title` ambiguous
|
|
17
|
+
* ("Open" → "Open customer"). Defaults to `title`. */
|
|
18
|
+
accessibilityLabel?: string;
|
|
19
|
+
tooltip?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A verb that lives INSIDE an inline field's surface — the Open on a reference,
|
|
25
|
+
* the Copy on a value worth copying — rather than in the row's trailing column.
|
|
26
|
+
*
|
|
27
|
+
* Inside is where it belongs when the act is ABOUT THE VALUE: it travels with
|
|
28
|
+
* what it acts on, so a reader never has to pair a control on the right edge of
|
|
29
|
+
* the page with the field it belongs to, and the trailing column stays free for
|
|
30
|
+
* row-level verbs.
|
|
31
|
+
*
|
|
32
|
+
* It is FILLED, on a field surface that is white. The contrast is the whole
|
|
33
|
+
* affordance, and it is a RELATIONSHIP rather than a colour: the verb must read
|
|
34
|
+
* as sitting ON the value, so it takes the tint the field does not. (Invert it
|
|
35
|
+
* again if the resting field surface ever changes, or the verb dissolves into
|
|
36
|
+
* the value it acts on.) It borrows `Button secondary`'s own fill, so the two
|
|
37
|
+
* read as one act at two scales; the kit's `Button` still cannot serve here,
|
|
38
|
+
* being the full 40px control height that this sits inside.
|
|
39
|
+
*
|
|
40
|
+
* NO BORDER and NO SHADOW: the fill alone separates it — a border would draw a
|
|
41
|
+
* box inside a box, now that the field has one — and depth belongs to the
|
|
42
|
+
* primary action alone (a drop shadow on a 28px control reads as a smudge, not
|
|
43
|
+
* lift).
|
|
44
|
+
*
|
|
45
|
+
* **It stops propagation.** That is the contract, not the styling: an inline
|
|
46
|
+
* button sits on a surface that is itself pressable (the field opens its editor,
|
|
47
|
+
* a reference opens its peek), so without this every press would fire both — the
|
|
48
|
+
* bug is silent, and it is the reason this is a primitive rather than a local
|
|
49
|
+
* composition.
|
|
50
|
+
*/
|
|
51
|
+
export function InlineButton(props: InlineButtonProps) {
|
|
52
|
+
const { title, onPress, accessibilityLabel, tooltip, disabled } = props;
|
|
53
|
+
return (
|
|
54
|
+
<PressableHighlight
|
|
55
|
+
focusRing
|
|
56
|
+
accessibilityRole="button"
|
|
57
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
58
|
+
tooltip={tooltip}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onPress={(e: GestureResponderEvent) => {
|
|
61
|
+
e?.stopPropagation?.();
|
|
62
|
+
onPress();
|
|
63
|
+
}}
|
|
64
|
+
style={(state) => [
|
|
65
|
+
{
|
|
66
|
+
height: INLINE_BUTTON_HEIGHT,
|
|
67
|
+
paddingHorizontal: 10,
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
// Softer than the 10 of the field it sits in: a control nested inside
|
|
70
|
+
// another reads as a distinct object when its corners are rounder than
|
|
71
|
+
// its host's, and at 28px tall a 10 would look square by comparison.
|
|
72
|
+
borderRadius: 8,
|
|
73
|
+
backgroundColor: state.hovered && !disabled ? colors.zinc[200] : colors.zinc[100],
|
|
74
|
+
opacity: disabled ? 0.5 : 1,
|
|
75
|
+
},
|
|
76
|
+
CONTROL_TRANSITION,
|
|
77
|
+
]}
|
|
78
|
+
>
|
|
79
|
+
<Text size="xs" weight="medium" color={disabled ? "muted" : "default"}>
|
|
80
|
+
{title}
|
|
81
|
+
</Text>
|
|
82
|
+
</PressableHighlight>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { Icon } from "./icon";
|
|
@@ -35,9 +35,14 @@ export interface InlineDatePickerProps {
|
|
|
35
35
|
* order. Defaults to the active `LoticsLocaleProvider` locale. */
|
|
36
36
|
locale?: string;
|
|
37
37
|
disabled?: boolean;
|
|
38
|
-
|
|
39
|
-
/** Form field (default) or grid cell — see `InlineEditVariant`. */
|
|
38
|
+
/** How much frame shows at rest — see {@link InlineEditVariant}. Default "framed". */
|
|
40
39
|
variant?: InlineEditVariant;
|
|
40
|
+
/** Verbs on the field's surface — an `InlineButton` Copy, Open… See
|
|
41
|
+
* `InlineEditView.actions`: they sit INSIDE the field, in the same place in
|
|
42
|
+
* every mode, and must be passed UNCONDITIONALLY (`disabled` the verb when it
|
|
43
|
+
* has nothing to act on) or the field resizes as the value fills. */
|
|
44
|
+
actions?: ReactNode;
|
|
45
|
+
accessibilityLabel?: string;
|
|
41
46
|
/** Colour the resting date by urgency — overdue red, soon-due amber (the caller owns
|
|
42
47
|
* the rule; e.g. a task's `dueTone`). Applies to a set value only. */
|
|
43
48
|
tone?: TextColor;
|
|
@@ -59,7 +64,7 @@ export interface InlineDatePickerProps {
|
|
|
59
64
|
* The row never changes height in either mode.
|
|
60
65
|
*/
|
|
61
66
|
export function InlineDatePicker(props: InlineDatePickerProps) {
|
|
62
|
-
const { value, onSave, onClear, format = "date", optionalTime, placeholder, locale, disabled, accessibilityLabel , variant,
|
|
67
|
+
const { value, onSave, onClear, format = "date", optionalTime, placeholder, locale, disabled, accessibilityLabel , tone, variant, actions } = props;
|
|
63
68
|
const locales = useLoticsLocale();
|
|
64
69
|
const inlineLabels = locales.inline;
|
|
65
70
|
const dateLabels = locales.datePicker;
|
|
@@ -236,11 +241,9 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
|
|
|
236
241
|
) : (
|
|
237
242
|
<Icon name={format === "datetime" ? "calendar-clock" : "calendar"} size={18} color={colors.zinc[400]} />
|
|
238
243
|
);
|
|
239
|
-
// The resting calendar glyph
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
// spinner still shows). The typed-edit field keeps its glyph — that's the open-picker button.
|
|
243
|
-
const restTrailing = variant === "cell" && !saving ? undefined : trailing;
|
|
244
|
+
// The resting calendar glyph marks an empty field as a DATE control — the same
|
|
245
|
+
// promise on every surface, now that a field looks the same everywhere.
|
|
246
|
+
const restTrailing = trailing;
|
|
244
247
|
|
|
245
248
|
return (
|
|
246
249
|
<View>
|
|
@@ -268,7 +271,11 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
|
|
|
268
271
|
<PopoverTrigger>
|
|
269
272
|
<InlineEditView
|
|
270
273
|
variant={variant}
|
|
271
|
-
|
|
274
|
+
actions={actions}
|
|
275
|
+
// The calendar anchors to the FIELD, not to the value region: with
|
|
276
|
+
// verbs on the surface the region inside is narrower than the box.
|
|
277
|
+
anchorRef={anchorRef}
|
|
278
|
+
tone={tone}
|
|
272
279
|
display={display}
|
|
273
280
|
placeholder={placeholder}
|
|
274
281
|
disabled={disabled}
|