@lotics/ui 7.19.0 → 7.19.2
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/package.json +1 -1
- package/src/agent_run.tsx +5 -3
- package/src/timeline.tsx +82 -53
package/AGENTS.md
CHANGED
|
@@ -655,13 +655,16 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
655
655
|
cross-cutting numbers, drop them. Do NOT put a `KPIStrip` on a register page or a `SummaryLine` on
|
|
656
656
|
a dashboard. Card stat rails use `KPICard`.
|
|
657
657
|
- **Numbers**: free-standing numerals `<Text tabular>`. Money: `formatMoney(n)` from
|
|
658
|
-
`@lotics/ui/format_money` (`compact` for strip captions
|
|
658
|
+
`@lotics/ui/format_money` (`compact` for strip captions; it emits a non-breaking space before `₫` —
|
|
659
|
+
normalize `\s` in test asserts). Dates: `formatDate(value, opts)` from
|
|
659
660
|
`@lotics/ui/format_date` — the date-VALUE formatter. `format` (date STYLE) ∈ `date` (22/05/2026) ·
|
|
660
661
|
`medium` (22 thg 5, 2026) · `long` (22 tháng 5, 2026) · `dayMonth` (22 thg 5) · `monthYear`
|
|
661
662
|
(Tháng 5 2026); **`time: true` is orthogonal** — prepends the 24h time to ANY style
|
|
662
663
|
(`14:30 22/05/2026`, `14:30 22 tháng 5, 2026`); `compact` drops the year on `date`. It's `Intl` +
|
|
663
664
|
the product's conventions (stable `/`, naive-ISO parse, 24h time-first, `emptyLabel`) — never
|
|
664
|
-
hand-roll a date via `Intl`/`padStart` for value display.
|
|
665
|
+
hand-roll a date via `Intl`/`padStart` for value display. An order the presets lack (e.g.
|
|
666
|
+
time-first `HH:mm dd/MM/yyyy` in a doc builder): `parseDate` + assemble the missing order —
|
|
667
|
+
never a regex re-implementation. (Exempt: a component's own internal
|
|
665
668
|
chrome — a calendar's header/weekday/a11y labels, a gantt axis — renders its own set.)
|
|
666
669
|
- **Every number is a door** (except the KPI strip). A component that summarizes records leads to
|
|
667
670
|
the records behind it when pressed — switch to the filtered list, expand in place, or navigate.
|
|
@@ -782,6 +785,10 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
782
785
|
A status is ONE family NAME (`{ color: "emerald" }`) and every weight derives from it. One accent
|
|
783
786
|
per screen purpose (blue=pipeline, emerald=money, red=danger, amber=waiting). Direct `colors.*` is
|
|
784
787
|
reserved for NEUTRALS (`zinc`, `border`, `white`, the `*[50]` selection washes).
|
|
788
|
+
**TEXT never wears `solid()`** — the 500 is a FILL shade (dots, series). A status word/number uses
|
|
789
|
+
`Text`'s valence tokens: `color="danger"` (red-900) / `"warning"` (amber-700) / `"success"`
|
|
790
|
+
(emerald-700) — AA on white. Map a dynamic `ColorName` to a token at the call site; hue nuance
|
|
791
|
+
(rose vs orange) stays on `Badge`s. Links use `Link`/`TextLink` (blue-600), never `solid("blue")`.
|
|
785
792
|
- **Status indicators have a WEIGHT — match to prominence, never to a metric.** colored `Text` (a
|
|
786
793
|
verdict WORD in a header) < `Badge variant="dot"` (a categorical STATE in a scannable column /
|
|
787
794
|
legend) < `Badge` tonal pill (a record's PROMINENT status, a drawer/detail header). A register's
|
package/package.json
CHANGED
package/src/agent_run.tsx
CHANGED
|
@@ -379,18 +379,20 @@ function ToolGroup(props: {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
const styles = StyleSheet.create({
|
|
382
|
-
|
|
382
|
+
// No horizontal self-inset: the run sits flush so the CONSUMER'S gutter governs
|
|
383
|
+
// alignment (an 8px inset made its text misalign with dialog titles / sibling
|
|
384
|
+
// lines in every padded container).
|
|
385
|
+
narration: {},
|
|
383
386
|
group: { gap: 2 },
|
|
384
387
|
row: {
|
|
385
388
|
flexDirection: "row",
|
|
386
389
|
alignItems: "center",
|
|
387
390
|
gap: 10,
|
|
388
391
|
minHeight: CONTROL_HEIGHT,
|
|
389
|
-
paddingHorizontal: 8,
|
|
390
392
|
borderRadius: CONTROL_RADIUS,
|
|
391
393
|
},
|
|
392
394
|
dotCol: { width: 18, alignItems: "center" },
|
|
393
395
|
rowBody: { flex: 1 },
|
|
394
396
|
// Reasoning body aligns under the "Thinking" label (past the dot column + gap).
|
|
395
|
-
reasoning: { paddingLeft:
|
|
397
|
+
reasoning: { paddingLeft: 28, paddingBottom: 4 },
|
|
396
398
|
});
|
package/src/timeline.tsx
CHANGED
|
@@ -46,20 +46,17 @@ export function Timeline(props: TimelineProps) {
|
|
|
46
46
|
{items.map((item, index) => {
|
|
47
47
|
const expanded = expandedIds[item.id] ?? false;
|
|
48
48
|
const hasDetails = !!item.details;
|
|
49
|
+
const hasBelow = !!item.description || !!item.error || (expanded && !!item.details);
|
|
50
|
+
const isFirst = index === 0;
|
|
51
|
+
const isLast = index === items.length - 1;
|
|
49
52
|
|
|
53
|
+
// The label row: label + right + chevron only. Secondary lines
|
|
54
|
+
// (description/error) render BELOW in the spine band, so the disc can
|
|
55
|
+
// center against this row whatever its height (a plain 20px label or a
|
|
56
|
+
// 40px inline control in `right` both align label-on-disc).
|
|
50
57
|
const row = (
|
|
51
|
-
<View style={
|
|
52
|
-
<
|
|
53
|
-
<Text size="sm">{item.label}</Text>
|
|
54
|
-
{item.description ? (
|
|
55
|
-
<Text size="xs" color="muted">{item.description}</Text>
|
|
56
|
-
) : null}
|
|
57
|
-
{item.error ? (
|
|
58
|
-
<Text size="xs" color="danger" numberOfLines={1}>
|
|
59
|
-
{item.error}
|
|
60
|
-
</Text>
|
|
61
|
-
) : null}
|
|
62
|
-
</View>
|
|
58
|
+
<View style={styles.row}>
|
|
59
|
+
<Text size="sm" style={{ flex: 1 }}>{item.label}</Text>
|
|
63
60
|
{item.right}
|
|
64
61
|
{hasDetails ? (
|
|
65
62
|
<Icon name={expanded ? "chevron-up" : "chevron-down"} size={14} color={colors.zinc[400]} />
|
|
@@ -68,30 +65,47 @@ export function Timeline(props: TimelineProps) {
|
|
|
68
65
|
);
|
|
69
66
|
|
|
70
67
|
return (
|
|
71
|
-
<View key={item.id}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
<View key={item.id}>
|
|
69
|
+
{/* Band 1 — disc beside the label row, vertically CENTERED on it.
|
|
70
|
+
The half-spines above/below the disc keep the line continuous
|
|
71
|
+
through tall rows. */}
|
|
72
|
+
<View style={styles.labelBand}>
|
|
73
|
+
<View style={styles.discColumn}>
|
|
74
|
+
<View style={styles.halfSpineSlot}>{!isFirst ? <View style={styles.spineFill} /> : null}</View>
|
|
75
|
+
<AnimationFadeIn key={`${item.id}-${item.icon}`}>
|
|
76
|
+
{/* Tinted disc: a low-alpha wash of the accent behind a full-color icon. */}
|
|
77
|
+
<View style={[styles.node, { backgroundColor: withAlpha(item.iconColor, 0.1) }]}>
|
|
78
|
+
{item.isLoading ? (
|
|
79
|
+
<ActivityIndicator size={13} color={item.iconColor} />
|
|
80
|
+
) : (
|
|
81
|
+
<Icon name={item.icon} size={13} color={item.iconColor} />
|
|
82
|
+
)}
|
|
83
|
+
</View>
|
|
84
|
+
</AnimationFadeIn>
|
|
85
|
+
<View style={styles.halfSpineSlot}>{!isLast ? <View style={styles.spineFill} /> : null}</View>
|
|
86
|
+
</View>
|
|
87
|
+
<View style={styles.rowColumn}>
|
|
88
|
+
{hasDetails ? (
|
|
89
|
+
<PressableHighlight focusRing onPress={() => toggleItem(item.id)} style={styles.pressableRow}>
|
|
90
|
+
{row}
|
|
91
|
+
</PressableHighlight>
|
|
92
|
+
) : (
|
|
93
|
+
<View style={styles.plainRow}>{row}</View>
|
|
94
|
+
)}
|
|
95
|
+
</View>
|
|
84
96
|
</View>
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
{/* Band 2 — description/error/details under the label, beside the
|
|
99
|
+
continuing spine; also carries the inter-item gap. */}
|
|
100
|
+
<View style={styles.belowBand}>
|
|
101
|
+
<View style={styles.spineColumn}>{!isLast ? <View style={styles.spineFill} /> : null}</View>
|
|
102
|
+
<View style={[styles.belowColumn, isLast && !hasBelow ? styles.belowColumnLast : null]}>
|
|
103
|
+
{item.description ? <Text size="xs" color="muted">{item.description}</Text> : null}
|
|
104
|
+
{item.error ? (
|
|
105
|
+
<Text size="xs" color="danger" numberOfLines={1}>{item.error}</Text>
|
|
106
|
+
) : null}
|
|
107
|
+
{expanded && item.details ? <View style={styles.detailsContainer}>{item.details}</View> : null}
|
|
108
|
+
</View>
|
|
95
109
|
</View>
|
|
96
110
|
</View>
|
|
97
111
|
);
|
|
@@ -101,16 +115,27 @@ export function Timeline(props: TimelineProps) {
|
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
const styles = StyleSheet.create({
|
|
104
|
-
|
|
118
|
+
labelBand: {
|
|
105
119
|
flexDirection: "row",
|
|
106
120
|
gap: 12,
|
|
107
121
|
},
|
|
108
|
-
|
|
122
|
+
// Stretches to the label row's height; the disc centers on it, half-spines
|
|
123
|
+
// fill the remainder so the line never breaks on a tall row.
|
|
124
|
+
discColumn: {
|
|
109
125
|
width: 24,
|
|
126
|
+
alignSelf: "stretch",
|
|
110
127
|
alignItems: "center",
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
},
|
|
129
|
+
halfSpineSlot: {
|
|
130
|
+
flex: 1,
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
justifyContent: "center",
|
|
133
|
+
},
|
|
134
|
+
spineFill: {
|
|
135
|
+
width: 1.5,
|
|
136
|
+
flex: 1,
|
|
137
|
+
borderRadius: 1,
|
|
138
|
+
backgroundColor: colors.zinc[200],
|
|
114
139
|
},
|
|
115
140
|
node: {
|
|
116
141
|
width: 24,
|
|
@@ -118,33 +143,36 @@ const styles = StyleSheet.create({
|
|
|
118
143
|
borderRadius: 12,
|
|
119
144
|
justifyContent: "center",
|
|
120
145
|
alignItems: "center",
|
|
146
|
+
marginVertical: 3,
|
|
121
147
|
},
|
|
122
|
-
|
|
123
|
-
width: 1.5,
|
|
148
|
+
rowColumn: {
|
|
124
149
|
flex: 1,
|
|
125
|
-
minHeight: 12,
|
|
126
|
-
borderRadius: 1,
|
|
127
|
-
backgroundColor: colors.zinc[200],
|
|
128
|
-
marginVertical: 3,
|
|
129
150
|
},
|
|
130
|
-
|
|
151
|
+
belowBand: {
|
|
152
|
+
flexDirection: "row",
|
|
153
|
+
gap: 12,
|
|
154
|
+
},
|
|
155
|
+
spineColumn: {
|
|
156
|
+
width: 24,
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
alignSelf: "stretch",
|
|
159
|
+
},
|
|
160
|
+
belowColumn: {
|
|
131
161
|
flex: 1,
|
|
162
|
+
gap: 2,
|
|
132
163
|
paddingBottom: 14,
|
|
133
164
|
},
|
|
165
|
+
belowColumnLast: {
|
|
166
|
+
paddingBottom: 0,
|
|
167
|
+
},
|
|
134
168
|
row: {
|
|
135
169
|
flexDirection: "row",
|
|
136
170
|
alignItems: "center",
|
|
137
171
|
gap: 10,
|
|
138
172
|
flex: 1,
|
|
139
173
|
},
|
|
140
|
-
// With a description the row is taller than one line — top-align so the
|
|
141
|
-
// timestamp (`right`) sits beside the label instead of centred on the block.
|
|
142
|
-
rowTop: {
|
|
143
|
-
alignItems: "flex-start",
|
|
144
|
-
},
|
|
145
174
|
// The press target for expandable rows; pressable rows bleed the hover wash
|
|
146
|
-
// past the text column.
|
|
147
|
-
// so the node lines up with the first line.
|
|
175
|
+
// past the text column.
|
|
148
176
|
pressableRow: {
|
|
149
177
|
borderRadius: 8,
|
|
150
178
|
paddingHorizontal: 8,
|
|
@@ -166,3 +194,4 @@ const styles = StyleSheet.create({
|
|
|
166
194
|
paddingLeft: 2,
|
|
167
195
|
},
|
|
168
196
|
});
|
|
197
|
+
|