@lotics/ui 47.13.0 → 47.14.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 +16 -2
- package/docs/catalog.md +225 -27
- package/docs/composition.md +83 -4
- package/docs/data_entry.md +17 -7
- package/docs/reviewing.md +30 -0
- package/docs/templates.md +1 -1
- package/examples/tpl_attendance.tsx +51 -80
- package/examples/tpl_pivot.tsx +10 -1
- package/examples/tpl_record.tsx +33 -107
- package/package.json +9 -1
- package/src/avatar_size.ts +9 -8
- package/src/axis_ticks.ts +63 -0
- package/src/bar_chart.tsx +69 -50
- package/src/cell_stack.tsx +43 -7
- package/src/chip_group.tsx +12 -6
- package/src/danger_zone.tsx +23 -10
- package/src/date_filter.tsx +17 -7
- package/src/date_filter_presets.ts +209 -1
- package/src/date_range_filter_field.tsx +14 -4
- package/src/detail_row.tsx +18 -18
- package/src/drawer.tsx +66 -28
- package/src/format_date.ts +31 -3
- package/src/index.css +18 -0
- package/src/ledger.tsx +23 -15
- package/src/line_chart.tsx +194 -61
- package/src/list_item.tsx +22 -3
- package/src/locale.tsx +25 -0
- package/src/matrix.tsx +116 -16
- package/src/matrix_totals.ts +71 -3
- package/src/month_math.ts +16 -0
- package/src/month_stepper.tsx +62 -0
- package/src/organization_name.ts +94 -0
- package/src/search_match.ts +19 -0
- package/src/section_nav.tsx +163 -0
- package/src/state_matrix.tsx +265 -0
- package/src/table.tsx +67 -5
- package/src/timetable.tsx +167 -0
- package/src/toggle_strip.tsx +259 -0
package/src/line_chart.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { View, StyleSheet, LayoutChangeEvent } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
-
import { useMemo, useState, useCallback } from "react";
|
|
5
|
-
import Svg, { Circle, Defs, Line, LinearGradient, Polygon, Polyline, Stop } from "react-native-svg";
|
|
4
|
+
import { Fragment, useMemo, useState, useCallback, useId } from "react";
|
|
5
|
+
import Svg, { Circle, Defs, G, Line, LinearGradient, Polygon, Polyline, Stop } from "react-native-svg";
|
|
6
|
+
import { LegendItem } from "./legend_item";
|
|
6
7
|
import { useLoticsLocale } from "./locale";
|
|
8
|
+
import { SPACE } from "./spacing";
|
|
7
9
|
import { axisLabelIndices } from "./axis_label_indices";
|
|
8
10
|
|
|
9
11
|
export interface LineChartPoint {
|
|
@@ -11,35 +13,109 @@ export interface LineChartPoint {
|
|
|
11
13
|
y: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export interface
|
|
16
|
+
export interface LineChartSeries {
|
|
17
|
+
key: string;
|
|
18
|
+
/** The series' name — its legend entry and the line's accessible name. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Omit to take the next colour off the chart's own order. */
|
|
21
|
+
color?: string;
|
|
15
22
|
points: LineChartPoint[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LineChartBaseProps {
|
|
16
26
|
height?: number;
|
|
17
27
|
formatNumber?: (n: number) => string;
|
|
18
28
|
formatXLabel?: (x: string | number) => string;
|
|
19
29
|
emptyLabel?: string;
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Render the series legend above the plot. Defaults to ON once there is more
|
|
32
|
+
* than one series — several lines with no key is a picture nobody can read —
|
|
33
|
+
* and OFF for a single one, which the card around it already names.
|
|
34
|
+
*/
|
|
35
|
+
legend?: boolean;
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
/**
|
|
39
|
+
* ONE series or SEVERAL, never both: `points` is the single-line form (with its
|
|
40
|
+
* own `lineColor`), `series` the multi-line one (each entry carries its own
|
|
41
|
+
* colour). Expressed as a union so passing both, or neither, fails to compile
|
|
42
|
+
* rather than resolving to whichever the body happens to read first.
|
|
43
|
+
*/
|
|
44
|
+
export type LineChartProps = LineChartBaseProps &
|
|
45
|
+
(
|
|
46
|
+
| { points: LineChartPoint[]; series?: undefined; lineColor?: string }
|
|
47
|
+
| { series: LineChartSeries[]; points?: undefined; lineColor?: undefined }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The order a series takes its colour in when it names none.
|
|
52
|
+
*
|
|
53
|
+
* Interleaved by hue for the reason `avatarTone`'s palette is: listed
|
|
54
|
+
* spectrally, four consecutive series come out four shades of one blue and the
|
|
55
|
+
* legend stops mapping anything. The first entry IS the single-line default, so
|
|
56
|
+
* one series drawn through `series` looks exactly like one drawn through
|
|
57
|
+
* `points`.
|
|
58
|
+
*
|
|
59
|
+
* Held here rather than shared with `PieChart`'s fallback because the two are
|
|
60
|
+
* different marks: a slice is a FILL and reads at 300, a line is a 2.5px STROKE
|
|
61
|
+
* and disappears at that shade.
|
|
62
|
+
*/
|
|
63
|
+
const SERIES_COLORS = [
|
|
64
|
+
colors.blue[500],
|
|
65
|
+
colors.orange[500],
|
|
66
|
+
colors.violet[500],
|
|
67
|
+
colors.emerald[600],
|
|
68
|
+
colors.fuchsia[600],
|
|
69
|
+
colors.teal[600],
|
|
70
|
+
colors.indigo[500],
|
|
71
|
+
colors.pink[600],
|
|
72
|
+
];
|
|
73
|
+
|
|
23
74
|
const defaultFormatNumber = (n: number): string =>
|
|
24
75
|
new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(n);
|
|
25
76
|
|
|
26
77
|
const defaultFormatXLabel = (x: string | number): string => String(x);
|
|
27
78
|
|
|
28
79
|
/**
|
|
29
|
-
* The canonical SVG line chart
|
|
30
|
-
*
|
|
80
|
+
* The canonical SVG line chart — `points: { x, y }[]` for one line, `series` for
|
|
81
|
+
* several on ONE SHARED y-scale (this period against last, a measure per site).
|
|
82
|
+
* `formatNumber`/`formatXLabel` for the axes, `height` for the plot. (No
|
|
83
|
+
* recharts.) For a tiny inline trend use `Sparkline`.
|
|
31
84
|
*/
|
|
32
85
|
export function LineChart(props: LineChartProps) {
|
|
33
86
|
const locale = useLoticsLocale();
|
|
34
87
|
const {
|
|
35
|
-
points
|
|
88
|
+
points,
|
|
89
|
+
series: seriesProp,
|
|
90
|
+
lineColor,
|
|
91
|
+
legend,
|
|
36
92
|
height: chartHeight = 160,
|
|
37
93
|
formatNumber = defaultFormatNumber,
|
|
38
94
|
formatXLabel = defaultFormatXLabel,
|
|
39
95
|
emptyLabel = locale.chart.noData,
|
|
40
|
-
lineColor = colors.blue[500],
|
|
41
96
|
} = props;
|
|
42
97
|
|
|
98
|
+
// A gradient id is document-global: a fixed one means the second chart on the
|
|
99
|
+
// page paints with the FIRST one's wash, which is invisible until two charts
|
|
100
|
+
// carry different colours and then reads as a rendering bug.
|
|
101
|
+
const areaId = `lineChartArea-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
102
|
+
|
|
103
|
+
// One code path from here down: the single-line form is a one-entry series
|
|
104
|
+
// list with no label (its name is the card's, not the line's).
|
|
105
|
+
const series = useMemo<LineChartSeries[]>(
|
|
106
|
+
() =>
|
|
107
|
+
seriesProp ??
|
|
108
|
+
[{ key: "value", label: "", color: lineColor, points: points ?? [] }],
|
|
109
|
+
[seriesProp, points, lineColor],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const colored = useMemo(
|
|
113
|
+
() => series.map((s, i) => ({ ...s, color: s.color ?? SERIES_COLORS[i % SERIES_COLORS.length] })),
|
|
114
|
+
[series],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const showLegend = legend ?? colored.length > 1;
|
|
118
|
+
|
|
43
119
|
const [chartWidth, setChartWidth] = useState(0);
|
|
44
120
|
|
|
45
121
|
const handleLayout = useCallback((event: LayoutChangeEvent) => {
|
|
@@ -51,11 +127,19 @@ export function LineChart(props: LineChartProps) {
|
|
|
51
127
|
const paddingLeft = 6;
|
|
52
128
|
const paddingRight = 10;
|
|
53
129
|
|
|
130
|
+
/** The categorical axis is as long as the LONGEST series; a shorter one ends early. */
|
|
131
|
+
const positionCount = useMemo(
|
|
132
|
+
() => colored.reduce((n, s) => Math.max(n, s.points.length), 0),
|
|
133
|
+
[colored],
|
|
134
|
+
);
|
|
135
|
+
|
|
54
136
|
// Nice domain AROUND the data — a 16–18% series must not be drawn against
|
|
55
137
|
// a 0–20 axis (it flattens into a sliver at the top). The baseline snaps
|
|
56
|
-
// to 0 only when the data actually lives near 0.
|
|
138
|
+
// to 0 only when the data actually lives near 0. Taken across EVERY series:
|
|
139
|
+
// one scale is what makes several lines comparable, and per-series scales
|
|
140
|
+
// would draw two different quantities as the same height.
|
|
57
141
|
const yAxisTicks = useMemo(() => {
|
|
58
|
-
const ys =
|
|
142
|
+
const ys = colored.flatMap((s) => s.points.map((p) => p.y));
|
|
59
143
|
const lo = Math.min(...ys, 0 === ys.length ? 0 : Infinity);
|
|
60
144
|
const hi = Math.max(...ys, lo);
|
|
61
145
|
const span = hi - lo || Math.abs(hi) || 1;
|
|
@@ -72,38 +156,42 @@ export function LineChart(props: LineChartProps) {
|
|
|
72
156
|
const ticks: number[] = [];
|
|
73
157
|
for (let t = min; t <= max + step / 1e6; t += step) ticks.push(Number(t.toFixed(10)));
|
|
74
158
|
return { ticks, min, max };
|
|
75
|
-
}, [
|
|
159
|
+
}, [colored]);
|
|
76
160
|
|
|
77
161
|
const { min: minValue, max: maxValue } = yAxisTicks;
|
|
78
162
|
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
163
|
+
const plotted = useMemo(() => {
|
|
164
|
+
if (positionCount === 0 || chartWidth === 0) return [];
|
|
81
165
|
|
|
82
166
|
const availableWidth = chartWidth - paddingLeft - paddingRight;
|
|
83
167
|
const availableHeight = chartHeight - paddingTop - paddingBottom;
|
|
84
168
|
const range = maxValue - minValue || 1;
|
|
85
169
|
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
170
|
+
return colored.map((s) => ({
|
|
171
|
+
...s,
|
|
172
|
+
drawn: s.points.map((point, index) => ({
|
|
173
|
+
x:
|
|
174
|
+
paddingLeft +
|
|
175
|
+
(positionCount > 1
|
|
176
|
+
? (index / (positionCount - 1)) * availableWidth
|
|
177
|
+
: availableWidth / 2),
|
|
178
|
+
y: paddingTop + availableHeight - ((point.y - minValue) / range) * availableHeight,
|
|
179
|
+
})),
|
|
180
|
+
}));
|
|
181
|
+
}, [colored, positionCount, chartWidth, minValue, maxValue, chartHeight]);
|
|
96
182
|
|
|
97
183
|
const visibleLabels = useMemo(() => {
|
|
98
|
-
if (
|
|
184
|
+
if (positionCount === 0 || chartWidth === 0) return [];
|
|
99
185
|
|
|
100
|
-
return axisLabelIndices(
|
|
101
|
-
|
|
102
|
-
label
|
|
103
|
-
|
|
104
|
-
|
|
186
|
+
return axisLabelIndices(positionCount, chartWidth).map((index) => {
|
|
187
|
+
// Whichever series reaches this position names it — the axis is shared,
|
|
188
|
+
// so the label is a fact about the position and not about one line.
|
|
189
|
+
const owner = colored.find((s) => s.points[index] !== undefined);
|
|
190
|
+
return { index, label: owner ? formatXLabel(owner.points[index].x) : "" };
|
|
191
|
+
});
|
|
192
|
+
}, [colored, positionCount, chartWidth, formatXLabel]);
|
|
105
193
|
|
|
106
|
-
if (
|
|
194
|
+
if (positionCount === 0) {
|
|
107
195
|
return (
|
|
108
196
|
<View style={styles.chartContainer}>
|
|
109
197
|
<Text color="muted">{emptyLabel}</Text>
|
|
@@ -111,9 +199,21 @@ export function LineChart(props: LineChartProps) {
|
|
|
111
199
|
);
|
|
112
200
|
}
|
|
113
201
|
|
|
202
|
+
// The area wash is a SINGLE-line device. Stacked translucent washes muddy
|
|
203
|
+
// into a colour that belongs to no series, and the line a reader is tracking
|
|
204
|
+
// then crosses a field of it — so several lines are drawn bare.
|
|
205
|
+
const single = colored.length === 1;
|
|
206
|
+
|
|
114
207
|
return (
|
|
115
208
|
<View style={styles.chartContainer}>
|
|
116
209
|
<View style={styles.chartWrapper}>
|
|
210
|
+
{showLegend ? (
|
|
211
|
+
<View style={styles.legend}>
|
|
212
|
+
{colored.map((s) => (
|
|
213
|
+
<LegendItem key={s.key} color={s.color} label={s.label} />
|
|
214
|
+
))}
|
|
215
|
+
</View>
|
|
216
|
+
) : null}
|
|
117
217
|
<View style={styles.chartRow}>
|
|
118
218
|
<View style={styles.yAxis}>
|
|
119
219
|
{yAxisTicks.ticks
|
|
@@ -128,13 +228,15 @@ export function LineChart(props: LineChartProps) {
|
|
|
128
228
|
<View style={[styles.svgContainer, { height: chartHeight }]} onLayout={handleLayout}>
|
|
129
229
|
{chartWidth > 0 && (
|
|
130
230
|
<Svg width={chartWidth} height={chartHeight}>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
231
|
+
{single && (
|
|
232
|
+
<Defs>
|
|
233
|
+
{/* soft area wash under the line — depth without decoration */}
|
|
234
|
+
<LinearGradient id={areaId} x1="0" y1="0" x2="0" y2="1">
|
|
235
|
+
<Stop offset="0" stopColor={plotted[0].color} stopOpacity={0.16} />
|
|
236
|
+
<Stop offset="1" stopColor={plotted[0].color} stopOpacity={0.01} />
|
|
237
|
+
</LinearGradient>
|
|
238
|
+
</Defs>
|
|
239
|
+
)}
|
|
138
240
|
{yAxisTicks.ticks.map((tick, i) => {
|
|
139
241
|
const availableHeight = chartHeight - paddingTop - paddingBottom;
|
|
140
242
|
const range = maxValue - minValue || 1;
|
|
@@ -151,30 +253,54 @@ export function LineChart(props: LineChartProps) {
|
|
|
151
253
|
/>
|
|
152
254
|
);
|
|
153
255
|
})}
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
256
|
+
{plotted.map((s) => {
|
|
257
|
+
const polylinePoints = s.drawn.map((p) => `${p.x},${p.y}`).join(" ");
|
|
258
|
+
const line = (
|
|
259
|
+
<>
|
|
260
|
+
{single && s.drawn.length > 1 && (
|
|
261
|
+
<Polygon
|
|
262
|
+
points={`${polylinePoints} ${s.drawn[s.drawn.length - 1].x},${chartHeight - paddingBottom} ${s.drawn[0].x},${chartHeight - paddingBottom}`}
|
|
263
|
+
fill={`url(#${areaId})`}
|
|
264
|
+
stroke="none"
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
{s.drawn.length > 1 && (
|
|
268
|
+
<Polyline
|
|
269
|
+
points={polylinePoints}
|
|
270
|
+
fill="none"
|
|
271
|
+
stroke={s.color}
|
|
272
|
+
strokeWidth={2.5}
|
|
273
|
+
strokeLinejoin="round"
|
|
274
|
+
strokeLinecap="round"
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
{s.drawn.map((point, index) => {
|
|
278
|
+
const isLast = index === s.drawn.length - 1;
|
|
279
|
+
// the latest value is the story — emphasize its point
|
|
280
|
+
if (isLast) {
|
|
281
|
+
return (
|
|
282
|
+
<Circle key={index} cx={point.x} cy={point.y} r={4.5} fill={s.color} stroke={colors.white} strokeWidth={2} />
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
// Intermediate markers multiply by the series count: four
|
|
286
|
+
// lines over twelve months is 48 discs, and the plot reads
|
|
287
|
+
// as a field of dots rather than as lines. Several series
|
|
288
|
+
// keep only the anchor the legend colour maps to.
|
|
289
|
+
return single ? (
|
|
290
|
+
<Circle key={index} cx={point.x} cy={point.y} r={3} fill={colors.white} stroke={s.color} strokeWidth={1.5} />
|
|
291
|
+
) : null;
|
|
292
|
+
})}
|
|
293
|
+
</>
|
|
294
|
+
);
|
|
295
|
+
// A named series announces itself; the single-line form has no
|
|
296
|
+
// name of its own, so it leaves the naming to its container
|
|
297
|
+
// rather than presenting an image called "".
|
|
298
|
+
return s.label ? (
|
|
299
|
+
<G key={s.key} accessibilityRole="image" accessibilityLabel={s.label}>
|
|
300
|
+
{line}
|
|
301
|
+
</G>
|
|
176
302
|
) : (
|
|
177
|
-
<
|
|
303
|
+
<Fragment key={s.key}>{line}</Fragment>
|
|
178
304
|
);
|
|
179
305
|
})}
|
|
180
306
|
</Svg>
|
|
@@ -183,7 +309,7 @@ export function LineChart(props: LineChartProps) {
|
|
|
183
309
|
</View>
|
|
184
310
|
<View style={styles.labelsContainer}>
|
|
185
311
|
{visibleLabels.map((item, i) => {
|
|
186
|
-
const position =
|
|
312
|
+
const position = positionCount > 1 ? item.index / (positionCount - 1) : 0.5;
|
|
187
313
|
return (
|
|
188
314
|
<Text
|
|
189
315
|
key={i}
|
|
@@ -218,6 +344,13 @@ const styles = StyleSheet.create({
|
|
|
218
344
|
chartWrapper: {
|
|
219
345
|
gap: 8,
|
|
220
346
|
},
|
|
347
|
+
// Above the plot, the same anatomy and spacing `StackedBarChart` gives its
|
|
348
|
+
// legend — two charts in one card must not annotate themselves two ways.
|
|
349
|
+
legend: {
|
|
350
|
+
flexDirection: "row",
|
|
351
|
+
flexWrap: "wrap",
|
|
352
|
+
gap: SPACE.md,
|
|
353
|
+
},
|
|
221
354
|
chartRow: {
|
|
222
355
|
flexDirection: "row",
|
|
223
356
|
gap: 4,
|
package/src/list_item.tsx
CHANGED
|
@@ -97,6 +97,7 @@ export function ListItem(props: ListItemProps) {
|
|
|
97
97
|
ref={ref}
|
|
98
98
|
testID={testID}
|
|
99
99
|
containerStyle={containerStyle}
|
|
100
|
+
selected={selected}
|
|
100
101
|
onPress={handlePress}
|
|
101
102
|
disabled={disabled}
|
|
102
103
|
right={right}
|
|
@@ -134,12 +135,14 @@ function PressRow(props: {
|
|
|
134
135
|
ref?: Ref<View>;
|
|
135
136
|
testID?: string;
|
|
136
137
|
containerStyle: StyleProp<ViewStyle>;
|
|
138
|
+
/** Selection outranks hover, so the pointer never erases which row is open. */
|
|
139
|
+
selected?: boolean;
|
|
137
140
|
onPress: () => void;
|
|
138
141
|
disabled?: boolean;
|
|
139
142
|
right?: React.ReactNode;
|
|
140
143
|
children: React.ReactNode;
|
|
141
144
|
}) {
|
|
142
|
-
const { ref, testID, containerStyle, onPress, disabled, right, children } = props;
|
|
145
|
+
const { ref, testID, containerStyle, selected, onPress, disabled, right, children } = props;
|
|
143
146
|
const [hovered, setHovered] = useState(false);
|
|
144
147
|
const [pressed, setPressed] = useState(false);
|
|
145
148
|
|
|
@@ -147,8 +150,11 @@ function PressRow(props: {
|
|
|
147
150
|
<View
|
|
148
151
|
style={[
|
|
149
152
|
containerStyle,
|
|
153
|
+
// SELECTION OUTRANKS HOVER. A selected row keeps its own (deeper) ground
|
|
154
|
+
// under the pointer; only an unselected one takes the lighter wash, so
|
|
155
|
+
// reaching for a neighbour never hides which row is open.
|
|
150
156
|
!disabled && pressed && styles.pressed,
|
|
151
|
-
!disabled && !pressed && hovered && styles.hovered,
|
|
157
|
+
!disabled && !pressed && hovered && !selected && styles.hovered,
|
|
152
158
|
]}
|
|
153
159
|
>
|
|
154
160
|
<FocusRingPressable
|
|
@@ -193,13 +199,26 @@ const styles = StyleSheet.create({
|
|
|
193
199
|
flex: 1,
|
|
194
200
|
alignItems: "flex-start",
|
|
195
201
|
},
|
|
202
|
+
// SELECTION AND HOVER MUST NOT PAINT THE SAME GREY. They did — both
|
|
203
|
+
// `zinc-100` — so on a navigation pane, pointing at any row made it
|
|
204
|
+
// indistinguishable from the row currently open, and the persistent state was
|
|
205
|
+
// erased by the transient one exactly while the reader was choosing between
|
|
206
|
+
// them. Every other probe reads a RESTING screen, which is why it survived: at
|
|
207
|
+
// rest the selected row is the only grey one and looks perfect.
|
|
208
|
+
//
|
|
209
|
+
// The ladder is `PressableRow`'s, verbatim — ONE NEUTRAL STEP apart, hover
|
|
210
|
+
// LIGHTER than selection: zinc-50 hovered, zinc-100 selected, zinc-200
|
|
211
|
+
// pressed. The kit had already reached that answer for the register row and
|
|
212
|
+
// written down why an edge or a brand ground is wrong for a ROW; a second
|
|
213
|
+
// answer here would give one question two selection languages on surfaces the
|
|
214
|
+
// same reader crosses.
|
|
196
215
|
selected: {
|
|
197
216
|
backgroundColor: colors.zinc["100"],
|
|
198
217
|
},
|
|
199
218
|
// The wash `PressableHighlight` would have painted, moved to the wrapper so it
|
|
200
219
|
// covers the row rather than stopping at the slot.
|
|
201
220
|
hovered: {
|
|
202
|
-
backgroundColor: colors.zinc["
|
|
221
|
+
backgroundColor: colors.zinc["50"],
|
|
203
222
|
},
|
|
204
223
|
pressed: {
|
|
205
224
|
backgroundColor: colors.zinc["200"],
|
package/src/locale.tsx
CHANGED
|
@@ -6,6 +6,9 @@ import { type GanttLabels } from "./gantt/types";
|
|
|
6
6
|
import { type SocialPostPreviewLabels } from "./social_post_rules";
|
|
7
7
|
import { type PaginationLabels } from "./pagination";
|
|
8
8
|
import { type SortHeaderLabels } from "./sort_header";
|
|
9
|
+
import { type MonthStepperLabels } from "./month_stepper";
|
|
10
|
+
import { type StateMatrixLabels } from "./state_matrix";
|
|
11
|
+
import { type TimetableLabels } from "./timetable";
|
|
9
12
|
import { type ConfidenceLabels } from "./confidence";
|
|
10
13
|
import { type RemainderMeterLabels } from "./remainder_meter";
|
|
11
14
|
import { type DateRangeFilterFieldLabels } from "./date_range_filter_field";
|
|
@@ -41,6 +44,12 @@ export interface LoticsLocale {
|
|
|
41
44
|
pagination: Required<PaginationLabels>;
|
|
42
45
|
/** `SortHeader` a11y prefix + asc/desc suffixes. */
|
|
43
46
|
sortHeader: Required<SortHeaderLabels>;
|
|
47
|
+
/** `Timetable`'s three column headers. */
|
|
48
|
+
timetable: Required<TimetableLabels>;
|
|
49
|
+
/** `StateMatrix`'s pinned total header. */
|
|
50
|
+
stateMatrix: Required<StateMatrixLabels>;
|
|
51
|
+
/** `MonthStepper`'s two arrows. */
|
|
52
|
+
monthStepper: Required<MonthStepperLabels>;
|
|
44
53
|
/** `ReferenceField`'s peek footer — the link verbs (change / clear), the draft
|
|
45
54
|
* verbs (edit / save / saving / cancel) and the go-to. `openLabel`
|
|
46
55
|
* stays a per-instance PROP because it names the DESTINATION ("Open
|
|
@@ -92,6 +101,10 @@ export interface LoticsLocale {
|
|
|
92
101
|
chargeLines: { quantity: (label: string) => string; unitPrice: (label: string) => string; amount: (label: string) => string };
|
|
93
102
|
/** `Ledger`: the screen-reader name of a peekable row. */
|
|
94
103
|
ledger: { rowDetails: (label: string) => string };
|
|
104
|
+
/** `SectionNav`: the bar's fallback name when `activeKey` matches no item,
|
|
105
|
+
* and what the attention dot ANNOUNCES — a bare mark names nothing, and it
|
|
106
|
+
* is the only thing saying a section needs looking at. */
|
|
107
|
+
sectionNav: { sections: string; needsAttention: string; hasProblem: string };
|
|
95
108
|
/** `RunningLedger`: the closing line's label ("Số dư hiện tại", "Tồn kho
|
|
96
109
|
* hiện tại") — the ledger's one derived, always-current figure. */
|
|
97
110
|
runningLedger: { currentBalance: string };
|
|
@@ -337,6 +350,9 @@ export const en: LoticsLocale = {
|
|
|
337
350
|
ascending: ", ascending",
|
|
338
351
|
descending: ", descending",
|
|
339
352
|
},
|
|
353
|
+
timetable: { time: "Time", subject: "Scheduled", status: "Status" },
|
|
354
|
+
stateMatrix: { total: "Total" },
|
|
355
|
+
monthStepper: { previous: "Previous month", next: "Next month" },
|
|
340
356
|
referenceField: { open: "Open", change: "Change", clear: "Clear", edit: "Edit", save: "Save", saving: "Saving…", cancel: "Cancel" },
|
|
341
357
|
optionList: { selectAll: "Select all", deselectAll: "Deselect all", clear: "Clear", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…", someSelected: "some selected" },
|
|
342
358
|
picker: { emptyOption: "None" },
|
|
@@ -369,6 +385,7 @@ export const en: LoticsLocale = {
|
|
|
369
385
|
clarify: { otherPlaceholder: "Or type your own answer…", back: "Back", next: "Next", cancel: "Cancel", submit: "Submit" },
|
|
370
386
|
chargeLines: { quantity: (label) => `Quantity for ${label}`, unitPrice: (label) => `Unit price for ${label}`, amount: (label) => `Amount for ${label}` },
|
|
371
387
|
ledger: { rowDetails: (label) => `${label} details` },
|
|
388
|
+
sectionNav: { sections: "Sections", needsAttention: "Needs attention", hasProblem: "Has a problem" },
|
|
372
389
|
runningLedger: { currentBalance: "Current balance" },
|
|
373
390
|
stepper: { complete: "Complete step", progress: "Progress" },
|
|
374
391
|
stepProgress: {
|
|
@@ -415,6 +432,8 @@ export const en: LoticsLocale = {
|
|
|
415
432
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
416
433
|
today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow",
|
|
417
434
|
thisWeek: "This week", thisMonth: "This month", lastMonth: "Last month",
|
|
435
|
+
thisQuarter: "This quarter", lastQuarter: "Last quarter",
|
|
436
|
+
thisYear: "This year", lastYear: "Last year",
|
|
418
437
|
from: "From", to: "To",
|
|
419
438
|
selectDateRange: "Select date range", selectDate: "Select date",
|
|
420
439
|
clear: "Clear", done: "Done", placeholder: "All time",
|
|
@@ -564,6 +583,9 @@ export const vi: LoticsLocale = {
|
|
|
564
583
|
ascending: " (tăng dần)",
|
|
565
584
|
descending: " (giảm dần)",
|
|
566
585
|
},
|
|
586
|
+
timetable: { time: "Giờ", subject: "Phân công", status: "Tình trạng" },
|
|
587
|
+
stateMatrix: { total: "Tổng" },
|
|
588
|
+
monthStepper: { previous: "Tháng trước", next: "Tháng sau" },
|
|
567
589
|
referenceField: { open: "Mở", change: "Đổi", clear: "Bỏ chọn", edit: "Sửa", save: "Lưu", saving: "Đang lưu…", cancel: "Hủy" },
|
|
568
590
|
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…", someSelected: "một số đã chọn" },
|
|
569
591
|
picker: { emptyOption: "Không có" },
|
|
@@ -596,6 +618,7 @@ export const vi: LoticsLocale = {
|
|
|
596
618
|
clarify: { otherPlaceholder: "Hoặc nhập câu trả lời khác…", back: "Quay lại", next: "Tiếp", cancel: "Hủy", submit: "Gửi" },
|
|
597
619
|
chargeLines: { quantity: (label) => `Số lượng ${label}`, unitPrice: (label) => `Đơn giá ${label}`, amount: (label) => `Số tiền ${label}` },
|
|
598
620
|
ledger: { rowDetails: (label) => `Chi tiết ${label}` },
|
|
621
|
+
sectionNav: { sections: "Mục", needsAttention: "Cần xem lại", hasProblem: "Đang có lỗi" },
|
|
599
622
|
runningLedger: { currentBalance: "Số dư hiện tại" },
|
|
600
623
|
stepper: { complete: "Hoàn thành bước", progress: "Tiến trình" },
|
|
601
624
|
stepProgress: {
|
|
@@ -642,6 +665,8 @@ export const vi: LoticsLocale = {
|
|
|
642
665
|
year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
|
|
643
666
|
today: "Hôm nay", yesterday: "Hôm qua", tomorrow: "Ngày mai",
|
|
644
667
|
thisWeek: "Tuần này", thisMonth: "Tháng này", lastMonth: "Tháng trước",
|
|
668
|
+
thisQuarter: "Quý này", lastQuarter: "Quý trước",
|
|
669
|
+
thisYear: "Năm nay", lastYear: "Năm trước",
|
|
645
670
|
from: "Từ", to: "Đến",
|
|
646
671
|
selectDateRange: "Chọn khoảng ngày", selectDate: "Chọn ngày",
|
|
647
672
|
clear: "Xóa", done: "Xong", placeholder: "Tất cả thời gian",
|