@lotics/ui 47.6.0 → 47.7.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 +22 -3
- package/MIGRATION.md +61 -62
- package/docs/ai_patterns.md +9 -2
- package/docs/catalog.md +46 -6
- package/docs/composition.md +148 -10
- package/docs/data_entry.md +8 -0
- package/docs/reviewing.md +51 -2
- package/examples/tpl_attendance.tsx +1 -1
- package/package.json +3 -1
- package/src/alert.tsx +1 -1
- package/src/avatar.tsx +1 -1
- package/src/avatar.web.tsx +1 -1
- package/src/avatar_group.tsx +1 -1
- package/src/bar_chart.tsx +10 -1
- package/src/board.tsx +1 -1
- package/src/button.tsx +4 -4
- package/src/cell_stack.tsx +140 -0
- package/src/column_filter.tsx +1 -1
- package/src/combobox.tsx +2 -2
- package/src/comments_thread.tsx +2 -2
- package/src/count.tsx +1 -1
- package/src/date_calendar.tsx +2 -2
- package/src/date_filter.tsx +1 -1
- package/src/diff_value.tsx +2 -2
- package/src/eyebrow.tsx +1 -1
- package/src/file_thumbnail.tsx +2 -2
- package/src/file_thumbnail_grid.tsx +1 -1
- package/src/filter_band.tsx +18 -8
- package/src/filter_chip.tsx +5 -2
- package/src/finding.tsx +19 -2
- package/src/form_field.tsx +1 -1
- package/src/inline_date_picker.tsx +1 -1
- package/src/inline_edit.tsx +2 -1
- package/src/line_chart.tsx +3 -1
- package/src/list_item.tsx +1 -1
- package/src/loading.tsx +1 -1
- package/src/matrix.tsx +1 -1
- package/src/member_chip.tsx +1 -1
- package/src/member_profile_card.tsx +1 -1
- package/src/menu_list_item.tsx +2 -2
- package/src/metric.tsx +1 -1
- package/src/option_list.tsx +1 -1
- package/src/page_header.tsx +26 -25
- package/src/pagination.tsx +1 -1
- package/src/pie_chart.tsx +4 -2
- package/src/radio_picker.tsx +3 -3
- package/src/section_heading.tsx +74 -5
- package/src/segmented_control.tsx +1 -1
- package/src/select.tsx +1 -1
- package/src/shortcut_badge.tsx +1 -1
- package/src/stacked_bar_chart.tsx +1 -1
- package/src/switcher.tsx +28 -5
- package/src/table.tsx +46 -8
- package/src/tabs.tsx +2 -2
- package/src/text.tsx +2 -7
- package/src/text_ink.ts +96 -0
- package/src/text_link.tsx +1 -1
- package/src/text_utils.ts +1 -46
- package/src/thumbnail_stack.tsx +1 -1
- package/src/time_picker.tsx +1 -1
- package/src/timeline.tsx +1 -1
- package/src/tokens.ts +28 -4
- package/src/tooltip.tsx +1 -1
- package/src/uploading_thumbnail.tsx +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { Text, type TextSize } from "./text";
|
|
4
|
+
import { type TextColor } from "./text_ink";
|
|
5
|
+
|
|
6
|
+
export interface CellStackProps {
|
|
7
|
+
/** The subject — what this cell is ABOUT. */
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* The ONE line that qualifies the subject. Omit for a lone value.
|
|
11
|
+
*
|
|
12
|
+
* One, not several: a stack of three is no longer a value and its annotation,
|
|
13
|
+
* it is a row that wants to be a row.
|
|
14
|
+
*/
|
|
15
|
+
caption?: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Subject weight. `medium` is itself a channel of demotion between the two
|
|
18
|
+
* lines, so it decides the caption's rung — see the class doc.
|
|
19
|
+
*/
|
|
20
|
+
weight?: "regular" | "medium" | "semibold";
|
|
21
|
+
/**
|
|
22
|
+
* Set when the cell opens with an identity mark the CALLER draws (an avatar, a
|
|
23
|
+
* status dot, a file badge). A mark separates the pair exactly as `medium`
|
|
24
|
+
* does, so the caption keeps body size beside one.
|
|
25
|
+
*/
|
|
26
|
+
marked?: boolean;
|
|
27
|
+
/** The subject's rung. The pair takes ONE rung, so the caption follows it. */
|
|
28
|
+
size?: TextSize;
|
|
29
|
+
/** The subject's ink. The caption is always `muted` — it is supporting text. */
|
|
30
|
+
color?: TextColor;
|
|
31
|
+
/** Fixed-width numerals on BOTH lines, so a column of figures lines up. */
|
|
32
|
+
tabular?: boolean;
|
|
33
|
+
align?: "left" | "right" | "center";
|
|
34
|
+
/** Clamp each line. A register cell almost always wants 1. */
|
|
35
|
+
numberOfLines?: number;
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
testID?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A SUBJECT over one qualifying line — the commonest cell in any register, and
|
|
42
|
+
* the one shape the kit stated as a law and never shipped as a part.
|
|
43
|
+
*
|
|
44
|
+
* The rule lives in `docs/composition.md` §Typography and it has three moving
|
|
45
|
+
* pieces that have to agree; every hand-roll got at least one wrong, and the
|
|
46
|
+
* failure is invisible to every spacing probe because the excess height comes
|
|
47
|
+
* from the LINE BOX, so no margin and no gap appears anywhere to explain it.
|
|
48
|
+
* One app carried nine copies drifted three ways — 44px of stack around 26px of
|
|
49
|
+
* type, a ratio of 1.69, across three different gaps.
|
|
50
|
+
*
|
|
51
|
+
* **1. The caption's rung is decided by what ELSE separates the pair**, never by
|
|
52
|
+
* what the line carries. `color="muted"` is already one channel of demotion, so
|
|
53
|
+
* a size step is a second, and a second is only worth its cost where nothing
|
|
54
|
+
* else is doing the job. A `medium` subject or a leading identity mark separates
|
|
55
|
+
* the pair on its own, so the caption stays at body size — which matters most
|
|
56
|
+
* where that line is the DISAMBIGUATOR (which person, at which company), since
|
|
57
|
+
* `xs` demotes content the reader needs and 12px is where stacked diacritics
|
|
58
|
+
* crowd. With both lines regular and colour the only thing between them, the
|
|
59
|
+
* caption drops to `xs` or the two read as peers.
|
|
60
|
+
*
|
|
61
|
+
* **2. Both body-rung lines take `leading="tight"`.** The ramp's body rungs
|
|
62
|
+
* carry PROSE leading, right for a paragraph and wrong for two lines that are
|
|
63
|
+
* one object: it leaves ~10px of empty box between them. Where the caption is
|
|
64
|
+
* already `xs` it is already tight, so only the subject carries the prop.
|
|
65
|
+
*
|
|
66
|
+
* **3. The stack's `gap` is 0.** Each tight line box contributes 3px above and
|
|
67
|
+
* below, which is the 6px the pair wants; a `gap` on top is a third source of
|
|
68
|
+
* space between lines meant to read as one.
|
|
69
|
+
*
|
|
70
|
+
* A LONE line is deliberately NOT tightened. Tight leading is what closes a
|
|
71
|
+
* PAIR up; applied to a single line it just sits oddly against the prose around
|
|
72
|
+
* it, and there is no second line for it to be close to.
|
|
73
|
+
*
|
|
74
|
+
* A caption passed as a NODE keeps its own leading and ink — forcing either on
|
|
75
|
+
* somebody else's subtree would silently re-space something that is not a line
|
|
76
|
+
* of text at all.
|
|
77
|
+
*/
|
|
78
|
+
export function CellStack(props: CellStackProps) {
|
|
79
|
+
const {
|
|
80
|
+
title,
|
|
81
|
+
caption,
|
|
82
|
+
weight = "regular",
|
|
83
|
+
marked = false,
|
|
84
|
+
size = "sm",
|
|
85
|
+
color,
|
|
86
|
+
tabular,
|
|
87
|
+
align = "left",
|
|
88
|
+
numberOfLines,
|
|
89
|
+
style,
|
|
90
|
+
testID,
|
|
91
|
+
} = props;
|
|
92
|
+
|
|
93
|
+
const hasCaption = caption !== undefined && caption !== null && caption !== false;
|
|
94
|
+
// Rule 1 — the caption's rung, from what else is already separating the pair.
|
|
95
|
+
const captionSize: TextSize = weight !== "regular" || marked ? size : "xs";
|
|
96
|
+
// Rule 2 — tight closes a PAIR; `xs` is tight by construction, and a lone line
|
|
97
|
+
// has nothing to close up to.
|
|
98
|
+
const subjectTight = hasCaption ? "tight" : undefined;
|
|
99
|
+
const captionTight = captionSize === "xs" ? undefined : "tight";
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={[{ gap: 0, alignItems: ALIGN[align] }, style]} testID={testID}>
|
|
103
|
+
{typeof title === "string" || typeof title === "number" ? (
|
|
104
|
+
<Text
|
|
105
|
+
size={size}
|
|
106
|
+
weight={weight}
|
|
107
|
+
color={color}
|
|
108
|
+
leading={subjectTight}
|
|
109
|
+
tabular={tabular}
|
|
110
|
+
numberOfLines={numberOfLines}
|
|
111
|
+
>
|
|
112
|
+
{title}
|
|
113
|
+
</Text>
|
|
114
|
+
) : (
|
|
115
|
+
title
|
|
116
|
+
)}
|
|
117
|
+
{hasCaption
|
|
118
|
+
? typeof caption === "string" || typeof caption === "number"
|
|
119
|
+
? (
|
|
120
|
+
<Text
|
|
121
|
+
size={captionSize}
|
|
122
|
+
color="muted"
|
|
123
|
+
leading={captionTight}
|
|
124
|
+
tabular={tabular}
|
|
125
|
+
numberOfLines={numberOfLines}
|
|
126
|
+
>
|
|
127
|
+
{caption}
|
|
128
|
+
</Text>
|
|
129
|
+
)
|
|
130
|
+
: caption
|
|
131
|
+
: null}
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ALIGN = {
|
|
137
|
+
left: "flex-start",
|
|
138
|
+
right: "flex-end",
|
|
139
|
+
center: "center",
|
|
140
|
+
} as const satisfies Record<"left" | "right" | "center", ViewStyle["alignItems"]>;
|
package/src/column_filter.tsx
CHANGED
|
@@ -137,7 +137,7 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
137
137
|
onChange({ kind: "number", min, max: value?.kind === "number" ? value.max : null })
|
|
138
138
|
}
|
|
139
139
|
/>
|
|
140
|
-
<Text size="sm" color="
|
|
140
|
+
<Text size="sm" color="muted">
|
|
141
141
|
–
|
|
142
142
|
</Text>
|
|
143
143
|
<NumberInput
|
package/src/combobox.tsx
CHANGED
|
@@ -483,7 +483,7 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
483
483
|
<View style={styles.menu}>
|
|
484
484
|
{showRecentsHeader ? (
|
|
485
485
|
<View style={styles.sectionHeader}>
|
|
486
|
-
<Text size="xs" weight="medium" color="
|
|
486
|
+
<Text size="xs" weight="medium" color="muted">
|
|
487
487
|
{recentsLabel}
|
|
488
488
|
</Text>
|
|
489
489
|
</View>
|
|
@@ -506,7 +506,7 @@ export function ComboboxContent(props: ComboboxContentProps) {
|
|
|
506
506
|
list.searching ? (
|
|
507
507
|
emptyEl ?? (
|
|
508
508
|
<View style={styles.statusRow}>
|
|
509
|
-
<Text size="sm" color="
|
|
509
|
+
<Text size="sm" color="muted">
|
|
510
510
|
{emptyText}
|
|
511
511
|
</Text>
|
|
512
512
|
</View>
|
package/src/comments_thread.tsx
CHANGED
|
@@ -211,7 +211,7 @@ function CommentRow(props: CommentRowProps) {
|
|
|
211
211
|
</View>
|
|
212
212
|
<View style={{ flex: 1 }}>
|
|
213
213
|
<Text weight="medium">{displayName}</Text>
|
|
214
|
-
<Text size="sm" color="
|
|
214
|
+
<Text size="sm" color="muted">
|
|
215
215
|
{formatTimestamp(comment.created_at)}
|
|
216
216
|
{comment.updated_at !== comment.created_at ? ` (${labels.edited})` : ""}
|
|
217
217
|
</Text>
|
|
@@ -292,7 +292,7 @@ export function CommentList(props: CommentListProps) {
|
|
|
292
292
|
);
|
|
293
293
|
|
|
294
294
|
if (sorted.length === 0) {
|
|
295
|
-
return <>{props.emptyState ?? <Text color="
|
|
295
|
+
return <>{props.emptyState ?? <Text color="muted">{labels.empty}</Text>}</>;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
return (
|
package/src/count.tsx
CHANGED
package/src/date_calendar.tsx
CHANGED
|
@@ -74,7 +74,7 @@ function DayCell(props: {
|
|
|
74
74
|
>
|
|
75
75
|
<Text
|
|
76
76
|
size="sm"
|
|
77
|
-
color={props.isActive ? "
|
|
77
|
+
color={props.isActive ? "onInverse" : "default"}
|
|
78
78
|
userSelect="none"
|
|
79
79
|
weight={props.isToday ? "semibold" : "regular"}
|
|
80
80
|
>
|
|
@@ -436,7 +436,7 @@ function CalendarMonth(props: CalendarMonthProps) {
|
|
|
436
436
|
<View style={styles.weekHeader}>
|
|
437
437
|
{daysOfWeekLabels.map((day) => (
|
|
438
438
|
<View key={day} style={styles.dayHeaderCell}>
|
|
439
|
-
<Text size="xs" color="
|
|
439
|
+
<Text size="xs" color="muted">
|
|
440
440
|
{day}
|
|
441
441
|
</Text>
|
|
442
442
|
</View>
|
package/src/date_filter.tsx
CHANGED
|
@@ -96,7 +96,7 @@ function RangeBound(props: {
|
|
|
96
96
|
const { label, placeholder, bound, includeTime, localeTag, onTimeChange } = props;
|
|
97
97
|
return (
|
|
98
98
|
<View style={styles.rangeItem}>
|
|
99
|
-
<Text size="xs" color="
|
|
99
|
+
<Text size="xs" color="muted">
|
|
100
100
|
{label}
|
|
101
101
|
</Text>
|
|
102
102
|
<Text size="sm" weight="medium" color={bound.date ? "default" : "muted"}>
|
package/src/diff_value.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
|
-
import { type TextColor } from "./
|
|
5
|
+
import { type TextColor } from "./text_ink";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A VALUE THAT CHANGED — the atom of every review surface.
|
|
@@ -172,7 +172,7 @@ export function DiffValue(props: DiffValueProps) {
|
|
|
172
172
|
// zinc-600 — pixel-identical to the field LABEL beside it, so the one word
|
|
173
173
|
// saying a value is being deleted read as chrome. It describes an absence,
|
|
174
174
|
// so it sits below the label rather than level with it.
|
|
175
|
-
color={hasAfter ? TONE[tone] : "
|
|
175
|
+
color={hasAfter ? TONE[tone] : "inactive"}
|
|
176
176
|
weight={hasAfter ? "medium" : "regular"}
|
|
177
177
|
tabular={tabular}
|
|
178
178
|
numberOfLines={1}
|
package/src/eyebrow.tsx
CHANGED
package/src/file_thumbnail.tsx
CHANGED
|
@@ -420,7 +420,7 @@ export function DocumentCard(props: DocumentCardProps) {
|
|
|
420
420
|
{...focusProps}
|
|
421
421
|
>
|
|
422
422
|
<FileBadge mimeType={mimeType} size={badgeSize} isTemplate={isTemplate} />
|
|
423
|
-
<Text size="xs" numberOfLines={1} color="
|
|
423
|
+
<Text size="xs" numberOfLines={1} color="muted" style={styles.documentTileName} userSelect="none">
|
|
424
424
|
{filename}
|
|
425
425
|
</Text>
|
|
426
426
|
{overlay}
|
|
@@ -491,7 +491,7 @@ export function MediaCard(props: MediaCardProps) {
|
|
|
491
491
|
</View>
|
|
492
492
|
</View>
|
|
493
493
|
<View style={styles.documentCardFooter}>
|
|
494
|
-
<Text size="xs" numberOfLines={1} color="
|
|
494
|
+
<Text size="xs" numberOfLines={1} color="muted" userSelect="none">
|
|
495
495
|
{filename}
|
|
496
496
|
</Text>
|
|
497
497
|
<View style={[styles.typeLabel, { backgroundColor: color }]}>
|
|
@@ -219,7 +219,7 @@ function OverflowTile({ size, count, onPress }: { size: number; count: number; o
|
|
|
219
219
|
const box = { width: size, height: size, borderRadius: compact ? 7 : 10 };
|
|
220
220
|
const { focusVisible, focusProps } = useFocusRing();
|
|
221
221
|
const label = (
|
|
222
|
-
<Text size={compact ? "xs" : "sm"} weight="medium" color="
|
|
222
|
+
<Text size={compact ? "xs" : "sm"} weight="medium" color="muted" userSelect="none">
|
|
223
223
|
+{count}
|
|
224
224
|
</Text>
|
|
225
225
|
);
|
package/src/filter_band.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, useState, type ReactNode } from "react";
|
|
1
|
+
import { Children, createContext, useContext, useState, type ReactNode } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { Dialog, DialogFooter, DialogHeader, DialogHeaderTitle, DialogScrollArea } from "./dialog";
|
|
@@ -58,6 +58,14 @@ export function FilterBand(props: FilterBandProps) {
|
|
|
58
58
|
const t = useLoticsLocale().filterBand;
|
|
59
59
|
const { small } = useScreenSize();
|
|
60
60
|
const [open, setOpen] = useState(false);
|
|
61
|
+
// WHETHER THERE IS ANYTHING TO COLLAPSE. `Children.toArray` drops the nulls a
|
|
62
|
+
// caller renders for a facet that has nothing to offer — a status filter whose
|
|
63
|
+
// options are all in view, a round picker before any round is recorded — so a
|
|
64
|
+
// band that draws no chips wide must not draw a button narrow. It did: the wide
|
|
65
|
+
// branch renders `children` and gets nothing, the narrow one rendered the
|
|
66
|
+
// trigger unconditionally, and the two presentations stopped agreeing at exactly
|
|
67
|
+
// the width where a control that opens an empty sheet costs the most.
|
|
68
|
+
const hasFilters = Children.toArray(children).length > 0;
|
|
61
69
|
|
|
62
70
|
if (!small) {
|
|
63
71
|
return (
|
|
@@ -77,14 +85,16 @@ export function FilterBand(props: FilterBandProps) {
|
|
|
77
85
|
return (
|
|
78
86
|
<View testID={testID} style={{ flexDirection: "row", alignItems: "center", gap: SPACE.sm }}>
|
|
79
87
|
{search != null && <View style={{ flexGrow: 1, flexShrink: 1, minWidth: 0 }}>{search}</View>}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
{hasFilters && (
|
|
89
|
+
<Button
|
|
90
|
+
color="secondary"
|
|
91
|
+
title={activeCount > 0 ? `${t.filters} (${activeCount})` : t.filters}
|
|
92
|
+
onPress={() => setOpen(true)}
|
|
93
|
+
testID={testID ? `${testID}-filters` : undefined}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
86
96
|
{action}
|
|
87
|
-
<Dialog open={open} onOpenChange={setOpen} width="92%" maxWidth={520}>
|
|
97
|
+
<Dialog open={open && hasFilters} onOpenChange={setOpen} width="92%" maxWidth={520}>
|
|
88
98
|
<DialogHeader>
|
|
89
99
|
<DialogHeaderTitle>{t.filters}</DialogHeaderTitle>
|
|
90
100
|
</DialogHeader>
|
package/src/filter_chip.tsx
CHANGED
|
@@ -127,7 +127,10 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
127
127
|
aria-expanded={isOpen}
|
|
128
128
|
style={styles.rowHeader}
|
|
129
129
|
>
|
|
130
|
-
|
|
130
|
+
{/* `muted`, not the retired `zinc-700`. This row's LABEL names the facet and
|
|
131
|
+
the summary beside it carries the accent — a label at full ink would
|
|
132
|
+
compete with the value it introduces. */}
|
|
133
|
+
<Text size="sm" weight="medium" color="muted">{label}</Text>
|
|
131
134
|
<View style={styles.rowRight}>
|
|
132
135
|
{active ? (
|
|
133
136
|
typeof summary === "string" ? (
|
|
@@ -171,7 +174,7 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
171
174
|
// to answer "is anything on?" from across the screen, and ink is the
|
|
172
175
|
// only channel that carries at that distance. It is a state, not
|
|
173
176
|
// decoration, which is what makes it a legitimate place for brand.
|
|
174
|
-
color={active ? undefined : "
|
|
177
|
+
color={active ? undefined : "muted"}
|
|
175
178
|
style={active ? { color: colors.accent } : undefined}
|
|
176
179
|
numberOfLines={1}
|
|
177
180
|
>
|
package/src/finding.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Text } from "./text";
|
|
|
4
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
5
5
|
import { useLoticsLocale } from "./locale";
|
|
6
6
|
import { solid, type ColorName } from "./colors";
|
|
7
|
+
import { useContainerSize } from "./size_boundary";
|
|
7
8
|
|
|
8
9
|
export type FindingSeverity = "critical" | "warning" | "info" | "positive";
|
|
9
10
|
|
|
@@ -111,6 +112,15 @@ export interface FindingProps {
|
|
|
111
112
|
export function Finding(props: FindingProps) {
|
|
112
113
|
const words = useLoticsLocale().finding;
|
|
113
114
|
const readings = props.readings ?? [];
|
|
115
|
+
// The headline STACKS where the row is narrow. Side by side, a title long
|
|
116
|
+
// enough to wrap breaks AROUND the figure and the sentence reads as two
|
|
117
|
+
// fragments with a number interposed. It never overflows — it only wraps
|
|
118
|
+
// mid-sentence — so every clipping probe passes, and at desk width the title
|
|
119
|
+
// does not wrap at all, which is why the defect is invisible at the width most
|
|
120
|
+
// reviews measure. The trade recorded below (a clipped delta is worse than a
|
|
121
|
+
// wrapped title) is about CLIPPING and still holds; stacking honours it, since
|
|
122
|
+
// a stacked figure is never clipped either.
|
|
123
|
+
const stacked = useContainerSize().small;
|
|
114
124
|
|
|
115
125
|
return (
|
|
116
126
|
<View style={[styles.root, props.style]}>
|
|
@@ -120,8 +130,8 @@ export function Finding(props: FindingProps) {
|
|
|
120
130
|
/>
|
|
121
131
|
|
|
122
132
|
<View style={styles.body}>
|
|
123
|
-
<View style={styles.headline}>
|
|
124
|
-
<Text size="sm" weight="medium" style={styles.title}>
|
|
133
|
+
<View style={stacked ? styles.headlineStacked : styles.headline}>
|
|
134
|
+
<Text size="sm" weight="medium" style={stacked ? styles.titleStacked : styles.title}>
|
|
125
135
|
{props.title}
|
|
126
136
|
</Text>
|
|
127
137
|
{props.delta ? (
|
|
@@ -207,7 +217,14 @@ const styles = StyleSheet.create({
|
|
|
207
217
|
dot: { width: DOT, height: DOT, borderRadius: DOT / 2, marginTop: TITLE_INSET + (TITLE_LINE - DOT) / 2 },
|
|
208
218
|
body: { flex: 1, gap: 3, minWidth: 0 },
|
|
209
219
|
headline: { flexDirection: "row", alignItems: "baseline", gap: 12 },
|
|
220
|
+
// The narrow fork: the title owns its own lines, the figure sits under them on
|
|
221
|
+
// the same left edge as the dot's row — the scan becomes top-to-bottom, which
|
|
222
|
+
// is what a phone reads anyway.
|
|
223
|
+
headlineStacked: { flexDirection: "column", alignItems: "flex-start", gap: 3 },
|
|
210
224
|
title: { flex: 1, minWidth: 0 },
|
|
225
|
+
// `flex: 1` in a COLUMN grows the box vertically, not horizontally, so the
|
|
226
|
+
// stacked title takes width the other way.
|
|
227
|
+
titleStacked: { alignSelf: "stretch", minWidth: 0 },
|
|
211
228
|
// Never shrinks: the figure is the point of the row, and a clipped delta is
|
|
212
229
|
// worse than a title that wraps. Its COLOUR is the severity's, set on the
|
|
213
230
|
// instance — the dot marks the left edge and the figure the right, so a
|
package/src/form_field.tsx
CHANGED
|
@@ -84,7 +84,7 @@ export function FormField(props: FormFieldProps & { children: React.ReactNode })
|
|
|
84
84
|
</Text>
|
|
85
85
|
)}
|
|
86
86
|
{!!optional && (
|
|
87
|
-
<Text numberOfLines={1} size="sm" color="
|
|
87
|
+
<Text numberOfLines={1} size="sm" color="muted">
|
|
88
88
|
{resolvedOptionalLabel}
|
|
89
89
|
</Text>
|
|
90
90
|
)}
|
|
@@ -10,7 +10,7 @@ import { isoHasTime, resolveDateCommit } from "./date_picker_value";
|
|
|
10
10
|
import { formatDate } from "./format_date";
|
|
11
11
|
import { ActivityIndicator } from "./activity_indicator";
|
|
12
12
|
import { type InlineEditVariant, InlineEditView } from "./inline_edit";
|
|
13
|
-
import { type TextColor } from "./
|
|
13
|
+
import { type TextColor } from "./text_ink";
|
|
14
14
|
import { useLoticsLocale, useLocaleTag } from "./locale";
|
|
15
15
|
import { getInteractionModality } from "./interaction_modality";
|
|
16
16
|
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
package/src/inline_edit.tsx
CHANGED
|
@@ -7,7 +7,8 @@ import { ActivityIndicator } from "./activity_indicator";
|
|
|
7
7
|
import { FocusRingPressable } from "./focus_ring_pressable";
|
|
8
8
|
import { colors } from "./colors";
|
|
9
9
|
import { FOCUS_RING, CONTROL_RADIUS, CONTROL_PADDING_V, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
10
|
-
import { fontFamilyRegular, getInputTextStyle, INPUT_LETTER_SPACING, getMultilineInputHeight
|
|
10
|
+
import { fontFamilyRegular, getInputTextStyle, INPUT_LETTER_SPACING, getMultilineInputHeight } from "./text_utils";
|
|
11
|
+
import { getTextColor, type TextColor } from "./text_ink";
|
|
11
12
|
import { getInteractionModality } from "./interaction_modality";
|
|
12
13
|
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
13
14
|
import { trackCommit } from "./pending_commits";
|
package/src/line_chart.tsx
CHANGED
|
@@ -208,8 +208,10 @@ export function LineChart(props: LineChartProps) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const styles = StyleSheet.create({
|
|
211
|
+
// Content-sized, for the reason written out on `BarChart.chartContainer`: a
|
|
212
|
+
// `flex: 1` root resolves to a zero basis inside the vertical stack a card body
|
|
213
|
+
// is, and the chart then paints outside it.
|
|
211
214
|
chartContainer: {
|
|
212
|
-
flex: 1,
|
|
213
215
|
justifyContent: "center",
|
|
214
216
|
paddingVertical: 8,
|
|
215
217
|
},
|
package/src/list_item.tsx
CHANGED
|
@@ -79,7 +79,7 @@ export function ListItem(props: ListItemProps) {
|
|
|
79
79
|
)}
|
|
80
80
|
{!!description &&
|
|
81
81
|
(typeof description === "string" ? (
|
|
82
|
-
<Text size="sm" color="
|
|
82
|
+
<Text size="sm" color="muted" leading="tight" numberOfLines={1} userSelect="none">
|
|
83
83
|
{description}
|
|
84
84
|
</Text>
|
|
85
85
|
) : (
|
package/src/loading.tsx
CHANGED
package/src/matrix.tsx
CHANGED
|
@@ -213,7 +213,7 @@ function MatrixCell({ ctx, display, row, col, value }: MatrixCellProps) {
|
|
|
213
213
|
const label = `${row.label}, ${col.label}: ${formatValue(value)}`;
|
|
214
214
|
|
|
215
215
|
const content = showNumber ? (
|
|
216
|
-
<Text size="sm" weight="medium" tabular align="right" color={value === 0 ? "
|
|
216
|
+
<Text size="sm" weight="medium" tabular align="right" color={value === 0 ? "muted" : "default"}>
|
|
217
217
|
{value === 0 ? "" : formatValue(value)}
|
|
218
218
|
</Text>
|
|
219
219
|
) : null;
|
package/src/member_chip.tsx
CHANGED
|
@@ -174,7 +174,7 @@ export function MemberProfileCard(props: MemberProfileCardProps) {
|
|
|
174
174
|
size it was separated from its own identity line by WEIGHT alone,
|
|
175
175
|
which is a separate font file rather than a step, and the surface
|
|
176
176
|
measured 1.17x — flatter than a register with no page band. */}
|
|
177
|
-
<Text {...SUBJECT} color={inactive ? "
|
|
177
|
+
<Text {...SUBJECT} color={inactive ? "muted" : undefined} numberOfLines={2}>
|
|
178
178
|
{displayName}
|
|
179
179
|
</Text>
|
|
180
180
|
{identityText ? (
|
package/src/menu_list_item.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { colors } from "./colors";
|
|
|
5
5
|
import { CONTROL_RADIUS } from "./control_surface";
|
|
6
6
|
import { Icon, IconName } from "./icon";
|
|
7
7
|
import { PressableHighlight } from "./pressable_highlight";
|
|
8
|
-
import type { TextColor } from "./
|
|
8
|
+
import type { TextColor } from "./text_ink";
|
|
9
9
|
|
|
10
10
|
export interface MenuListItemProps {
|
|
11
11
|
ref?: Ref<View>;
|
|
@@ -77,7 +77,7 @@ export function MenuListItem(props: MenuListItemProps) {
|
|
|
77
77
|
title
|
|
78
78
|
)}
|
|
79
79
|
{!!description && (
|
|
80
|
-
<Text size="xs" color="
|
|
80
|
+
<Text size="xs" color="muted" numberOfLines={1} userSelect="none">
|
|
81
81
|
{description}
|
|
82
82
|
</Text>
|
|
83
83
|
)}
|
package/src/metric.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { View, StyleSheet, type TextStyle } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
-
import { getTextColor } from "./
|
|
4
|
+
import { getTextColor } from "./text_ink";
|
|
5
5
|
import { formatCompactNumber, formatMoney } from "./format_money";
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
|
package/src/option_list.tsx
CHANGED
|
@@ -141,7 +141,7 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
141
141
|
</View>
|
|
142
142
|
) : list.rows.length === 0 ? (
|
|
143
143
|
<View style={styles.statusRow}>
|
|
144
|
-
<Text color="
|
|
144
|
+
<Text color="muted" size="sm">
|
|
145
145
|
{emptyText}
|
|
146
146
|
</Text>
|
|
147
147
|
</View>
|
package/src/page_header.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
import { Text } from "@lotics/ui/text";
|
|
3
|
-
import { Spacer } from "@lotics/ui/spacer";
|
|
4
3
|
import { ReactNode } from "react";
|
|
5
4
|
|
|
6
5
|
interface PageHeaderProps {
|
|
@@ -62,28 +61,36 @@ export function PageHeader(props: PageHeaderProps) {
|
|
|
62
61
|
<View
|
|
63
62
|
style={{
|
|
64
63
|
flexDirection: "row",
|
|
65
|
-
|
|
64
|
+
// Top-aligned once a description makes the title block taller than the
|
|
65
|
+
// verbs; centring would hang them off the middle of a two-line column.
|
|
66
|
+
alignItems: description ? "flex-start" : "center",
|
|
66
67
|
flexWrap: "wrap",
|
|
67
68
|
gap: 12,
|
|
68
69
|
}}
|
|
69
70
|
>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
71
|
+
{/* THE DESCRIPTION RIDES THE TITLE, because it explains the title.
|
|
72
|
+
It used to render after the whole title row — and that row wraps, so at
|
|
73
|
+
a narrow width `actions` dropped to their own line and the reading order
|
|
74
|
+
became title → verbs → the sentence explaining them. That made `actions`
|
|
75
|
+
unusable on any header carrying a description, and the caller's only
|
|
76
|
+
recourse was to hand-roll a second toolbar row and pay for it in
|
|
77
|
+
vertical space at every width.
|
|
78
|
+
|
|
79
|
+
The title is OPTIONAL here: the shell delegates to this component for a
|
|
80
|
+
band that carries only a description, so an absent title must render
|
|
81
|
+
nothing rather than an empty heading line. */}
|
|
82
|
+
<View style={{ flexShrink: 1, minWidth: 0, gap: 4 }}>
|
|
83
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
84
|
+
{!!title && (
|
|
85
|
+
<Text size="xxl" weight="semibold" style={{ flexShrink: 1, minWidth: 0 }}>
|
|
86
|
+
{title}
|
|
87
|
+
</Text>
|
|
88
|
+
)}
|
|
89
|
+
{/* Never shrinks: the title is what gives way, and a control squeezed
|
|
90
|
+
below its own icon is not a smaller control, it is a broken one. */}
|
|
91
|
+
{trailing !== undefined && <View style={{ flexShrink: 0 }}>{trailing}</View>}
|
|
92
|
+
</View>
|
|
93
|
+
{!!description && <Text color="muted">{description}</Text>}
|
|
87
94
|
</View>
|
|
88
95
|
{/* Wrapped so which side gives way is STATED rather than left to whatever
|
|
89
96
|
the caller happened to pass. A bare `{actions}` inherited its own
|
|
@@ -148,12 +155,6 @@ export function PageHeader(props: PageHeaderProps) {
|
|
|
148
155
|
) : (
|
|
149
156
|
titleRow
|
|
150
157
|
)}
|
|
151
|
-
{!!description && (
|
|
152
|
-
<>
|
|
153
|
-
{!!title && <Spacer size={8} />}
|
|
154
|
-
<Text color="zinc-500">{description}</Text>
|
|
155
|
-
</>
|
|
156
|
-
)}
|
|
157
158
|
</View>
|
|
158
159
|
);
|
|
159
160
|
}
|
package/src/pagination.tsx
CHANGED
package/src/pie_chart.tsx
CHANGED
|
@@ -149,7 +149,7 @@ export function PieChart(props: PieChartProps) {
|
|
|
149
149
|
{item.label}
|
|
150
150
|
</Text>
|
|
151
151
|
<Text size="sm" tabular style={styles.legendValue}>{formatNumber(item.value)}</Text>
|
|
152
|
-
<Text size="sm" color="
|
|
152
|
+
<Text size="sm" color="muted" tabular style={styles.legendPercentage}>
|
|
153
153
|
{percentage.toFixed(0)}%
|
|
154
154
|
</Text>
|
|
155
155
|
</View>
|
|
@@ -163,8 +163,10 @@ export function PieChart(props: PieChartProps) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const styles = StyleSheet.create({
|
|
166
|
+
// Content-sized, for the reason written out on `BarChart.chartContainer`: a
|
|
167
|
+
// `flex: 1` root resolves to a zero basis inside the vertical stack a card body
|
|
168
|
+
// is, and the chart then paints outside it.
|
|
166
169
|
chartContainer: {
|
|
167
|
-
flex: 1,
|
|
168
170
|
justifyContent: "center",
|
|
169
171
|
},
|
|
170
172
|
chartWrapper: {
|