@lotics/ui 7.3.0 → 7.5.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 +10 -2
- package/examples/tpl_dashboard.tsx +17 -0
- package/package.json +2 -1
- package/src/form_field.tsx +6 -3
- package/src/funnel.tsx +218 -0
- package/src/locale.tsx +9 -5
- package/src/option_list.tsx +1 -1
package/AGENTS.md
CHANGED
|
@@ -20,8 +20,8 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
|
|
|
20
20
|
per-instance `labels`/`clearLabel`/`selectAllLabel` still overrides for one-offs, and an un-wrapped
|
|
21
21
|
app stays English. Wired so far: `Pagination`, `SortHeader`/`Table.sortLabels`, `OptionList`
|
|
22
22
|
(+ `Select`/`Combobox` built on it), `FilterChip`, `Drawer`, `Confidence`, `RemainderMeter`,
|
|
23
|
-
`ChangeReview`/`ChangeReviewActions`,
|
|
24
|
-
time-segment a11y, one `dateRange` slice). Adding a string to a wired component's `*Labels` forces
|
|
23
|
+
`ChangeReview`/`ChangeReviewActions`, `DateRangeFilterField`/`DateFilter` (presets + footer +
|
|
24
|
+
time-segment a11y, one `dateRange` slice), and `FormField` (the `optional` marker). Adding a string to a wired component's `*Labels` forces
|
|
25
25
|
both packs in `locale.tsx` to fill it (compile error) — that's how the kit avoids a silent English
|
|
26
26
|
leak. Not yet on the provider (pass `labels` for now): calendar/gantt, the file/preview/comment labels.
|
|
27
27
|
|
|
@@ -97,6 +97,13 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
97
97
|
recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
|
|
98
98
|
(a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
|
|
99
99
|
"Show N more" toggle — `labels` to localize — so several facet cards align to one height in a row),
|
|
100
|
+
`Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step conversion rate is the
|
|
101
|
+
HEADLINE (a bold aligned row across the top, the first stage = the 100% baseline), the count is the
|
|
102
|
+
supporting figure below — the Amplitude/Mixpanel convention, never pin the rate to the fill height.
|
|
103
|
+
`orientation` vertical columns | horizontal bars; pass `onSelect`+`selectedKey` to make the bars
|
|
104
|
+
press-to-drill (the selected stays solid, others dim — the caller renders the records). The
|
|
105
|
+
subset/drop-off sibling of `StackedProgressBar` — nested cohorts that shrink "calls → connected →
|
|
106
|
+
won", NOT a whole split across stages — that's `StackedProgressBar`),
|
|
100
107
|
`StatusGrid` + `StatusLegend`, `Heatmap` (density: colour-only, "where does it cluster"),
|
|
101
108
|
`Matrix` (the PIVOT cross-tab: the NUMBER in each cell — optionally a heat wash behind it — plus
|
|
102
109
|
row/column/grand totals; press a cell to drill).
|
|
@@ -801,6 +808,7 @@ step_progress · timeline (heterogeneous event LOG — per-row icon + expandable
|
|
|
801
808
|
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
802
809
|
CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
|
|
803
810
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
811
|
+
funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
|
|
804
812
|
status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
|
|
805
813
|
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done) + built-in gallery + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `labels`, `galleryLabels`); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview ·
|
|
806
814
|
file_gallery_modal · image_gallery · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
@@ -32,6 +32,7 @@ import { type IconName } from "@lotics/ui/icon";
|
|
|
32
32
|
import { KPIStrip } from "@lotics/ui/kpi_strip";
|
|
33
33
|
import { Peek } from "@lotics/ui/peek";
|
|
34
34
|
import { StackedProgressBar } from "@lotics/ui/stacked_progress_bar";
|
|
35
|
+
import { Funnel } from "@lotics/ui/funnel";
|
|
35
36
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
36
37
|
|
|
37
38
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -409,6 +410,22 @@ export function TplDashboard() {
|
|
|
409
410
|
</Card>
|
|
410
411
|
</View>
|
|
411
412
|
|
|
413
|
+
{/* chart row 3: the SAME pipeline as a CONVERSION funnel — narrowing
|
|
414
|
+
cohorts + step drop-off rates, the subset lens vs the composition bar above */}
|
|
415
|
+
<View style={{ flexDirection: "row", gap: 16 }}>
|
|
416
|
+
<Card style={{ padding: 0, flex: 1 }}>
|
|
417
|
+
<CardHeader>
|
|
418
|
+
<CardHeaderTitle info="Of all prospects, how many reach each stage — the drop-off from first contact to a closed win">
|
|
419
|
+
Conversion funnel
|
|
420
|
+
</CardHeaderTitle>
|
|
421
|
+
<CardHeaderMeta>This quarter</CardHeaderMeta>
|
|
422
|
+
</CardHeader>
|
|
423
|
+
<View style={{ paddingHorizontal: 20, paddingVertical: 16 }}>
|
|
424
|
+
<Funnel stages={PHEU_STAGES.map((s) => ({ key: s.key, label: s.label, value: s.count }))} color={solid("blue")} />
|
|
425
|
+
</View>
|
|
426
|
+
</Card>
|
|
427
|
+
</View>
|
|
428
|
+
|
|
412
429
|
{/* needs attention — each row expands in place to the records behind its count */}
|
|
413
430
|
<Card style={{ padding: 0 }}>
|
|
414
431
|
<CardHeader>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"./stacked_progress_bar": "./src/stacked_progress_bar.tsx",
|
|
54
54
|
"./legend_item": "./src/legend_item.tsx",
|
|
55
55
|
"./breakdown": "./src/breakdown.tsx",
|
|
56
|
+
"./funnel": "./src/funnel.tsx",
|
|
56
57
|
"./status_grid": "./src/status_grid.tsx",
|
|
57
58
|
"./heatmap": "./src/heatmap.tsx",
|
|
58
59
|
"./matrix": "./src/matrix.tsx",
|
package/src/form_field.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { createContext, useContext, useId } from "react";
|
|
|
2
2
|
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { Spacer } from "./spacer";
|
|
5
|
+
import { useLoticsLocale } from "./locale";
|
|
5
6
|
|
|
6
7
|
export interface FormFieldProps {
|
|
7
8
|
label: string;
|
|
@@ -9,7 +10,8 @@ export interface FormFieldProps {
|
|
|
9
10
|
description?: string;
|
|
10
11
|
error?: string;
|
|
11
12
|
style?: StyleProp<ViewStyle>;
|
|
12
|
-
/** Label shown next to `label` when `optional` is true.
|
|
13
|
+
/** Label shown next to `label` when `optional` is true. Defaults to the locale
|
|
14
|
+
* pack's `formField.optional` ("Optional"); pass to override for one instance. */
|
|
13
15
|
optionalLabel?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -40,7 +42,8 @@ export function useFormField(): FormFieldBinding | null {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function FormField(props: FormFieldProps & { children: React.ReactNode }) {
|
|
43
|
-
const { label, description, error, optional, style, children, optionalLabel
|
|
45
|
+
const { label, description, error, optional, style, children, optionalLabel } = props;
|
|
46
|
+
const resolvedOptionalLabel = optionalLabel ?? useLoticsLocale().formField.optional;
|
|
44
47
|
|
|
45
48
|
const inputId = useId();
|
|
46
49
|
const labelId = `${inputId}-label`;
|
|
@@ -72,7 +75,7 @@ export function FormField(props: FormFieldProps & { children: React.ReactNode })
|
|
|
72
75
|
)}
|
|
73
76
|
{!!optional && (
|
|
74
77
|
<Text numberOfLines={1} size="sm" color="zinc-500">
|
|
75
|
-
{
|
|
78
|
+
{resolvedOptionalLabel}
|
|
76
79
|
</Text>
|
|
77
80
|
)}
|
|
78
81
|
</View>
|
package/src/funnel.tsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { Text } from "./text";
|
|
5
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
6
|
+
|
|
7
|
+
export interface FunnelStage {
|
|
8
|
+
/** Identifier — the React key + the `onSelect` value. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** Stage name (the widest / top-of-funnel stage first). */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Count at this stage. Expected to be non-increasing down the list. */
|
|
13
|
+
value: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FunnelProps {
|
|
17
|
+
/** Ordered stages, widest (top of funnel) first. Each bar is sized `value ÷
|
|
18
|
+
* stages[0].value`, so the bars narrow through the funnel and the drop-off
|
|
19
|
+
* IS the shape. */
|
|
20
|
+
stages: FunnelStage[];
|
|
21
|
+
/** `vertical` = columns side-by-side that fill the width evenly, height ∝
|
|
22
|
+
* value capped at `size`; `horizontal` = stacked rows, bar width ∝ value.
|
|
23
|
+
* Default `vertical`. */
|
|
24
|
+
orientation?: "vertical" | "horizontal";
|
|
25
|
+
/** Bar fill — one accent for the whole funnel (one cohort narrowing, not many
|
|
26
|
+
* categories). Defaults to the neutral ink. */
|
|
27
|
+
color?: string;
|
|
28
|
+
/** Format a stage value (default `toLocaleString`). */
|
|
29
|
+
formatValue?: (n: number) => string;
|
|
30
|
+
/** Format the step conversion rate beside a stage (its value ÷ the stage
|
|
31
|
+
* above). Default `"NN%"`; return `null` to hide a rate. */
|
|
32
|
+
formatRate?: (rate: number) => string | null;
|
|
33
|
+
/** Make the bars press-to-drill: pressing a stage calls `onSelect(key)`, and
|
|
34
|
+
* pressing the selected stage again calls `onSelect(null)`. Pair with
|
|
35
|
+
* `selectedKey` — the selected bar stays solid, the others dim. The drill
|
|
36
|
+
* CONTENT (the records behind the stage) is the caller's to render. */
|
|
37
|
+
onSelect?: (key: string | null) => void;
|
|
38
|
+
/** The drilled stage (dims the others). */
|
|
39
|
+
selectedKey?: string | null;
|
|
40
|
+
/** xs muted uppercase eyebrow above the funnel. Omit inside a band that
|
|
41
|
+
* already names it. */
|
|
42
|
+
title?: string;
|
|
43
|
+
/** Vertical: the column area height (the cap). Horizontal: bar thickness.
|
|
44
|
+
* Defaults 160 / 28. */
|
|
45
|
+
size?: number;
|
|
46
|
+
accessibilityLabel?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultFormatValue = (n: number): string => n.toLocaleString("en-US");
|
|
50
|
+
const defaultFormatRate = (rate: number): string => `${Math.round(rate * 100)}%`;
|
|
51
|
+
const RADIUS = 2;
|
|
52
|
+
const DIM = 0.28;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Conversion funnel — ordered stages as bars that NARROW (each sized value ÷
|
|
56
|
+
* the top stage), with the step conversion rate (value ÷ the stage above)
|
|
57
|
+
* beside each. The drop-off is the shape, so it reads "where do they fall
|
|
58
|
+
* out?" at a glance. `vertical` columns or `horizontal` bars; pass `onSelect`
|
|
59
|
+
* to make the bars press-to-drill (select/deselect, the others dim).
|
|
60
|
+
*
|
|
61
|
+
* The SUBSET/conversion sibling of `StackedProgressBar`: a funnel is nested
|
|
62
|
+
* cohorts that shrink (calls → connected → won), NOT a composition of a whole
|
|
63
|
+
* (StackedProgressBar — items distributed ACROSS stages that sum to a total).
|
|
64
|
+
*/
|
|
65
|
+
export function Funnel(props: FunnelProps) {
|
|
66
|
+
const {
|
|
67
|
+
stages,
|
|
68
|
+
orientation = "vertical",
|
|
69
|
+
color = colors.zinc[800],
|
|
70
|
+
formatValue = defaultFormatValue,
|
|
71
|
+
formatRate = defaultFormatRate,
|
|
72
|
+
onSelect,
|
|
73
|
+
selectedKey = null,
|
|
74
|
+
title,
|
|
75
|
+
size = orientation === "vertical" ? 160 : 28,
|
|
76
|
+
accessibilityLabel,
|
|
77
|
+
} = props;
|
|
78
|
+
|
|
79
|
+
const top = stages[0]?.value ?? 0;
|
|
80
|
+
const fractionOf = (v: number) => (top > 0 ? Math.max(0, Math.min(1, v / top)) : 0);
|
|
81
|
+
const rateOf = (i: number) => {
|
|
82
|
+
const prev = i > 0 ? stages[i - 1].value : null;
|
|
83
|
+
return prev != null && prev > 0 ? formatRate(stages[i].value / prev) : null;
|
|
84
|
+
};
|
|
85
|
+
const fillOpacity = (key: string) => (selectedKey != null && selectedKey !== key ? DIM : 1);
|
|
86
|
+
|
|
87
|
+
// A bar is pressable only when onSelect is wired; press the selected one again
|
|
88
|
+
// to clear. `fill` makes the press target span the column (vertical).
|
|
89
|
+
const press = (key: string, label: string, fill: boolean, node: ReactNode) => {
|
|
90
|
+
if (!onSelect) return node;
|
|
91
|
+
return (
|
|
92
|
+
<PressableHighlight
|
|
93
|
+
accessibilityRole="button"
|
|
94
|
+
accessibilityState={{ selected: selectedKey === key }}
|
|
95
|
+
accessibilityLabel={label}
|
|
96
|
+
onPress={() => onSelect(selectedKey === key ? null : key)}
|
|
97
|
+
style={fill ? styles.pressFill : undefined}
|
|
98
|
+
>
|
|
99
|
+
{node}
|
|
100
|
+
</PressableHighlight>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const titleNode = title ? (
|
|
105
|
+
<Text size="xs" color="muted" transform="uppercase">
|
|
106
|
+
{title}
|
|
107
|
+
</Text>
|
|
108
|
+
) : null;
|
|
109
|
+
|
|
110
|
+
if (orientation === "vertical") {
|
|
111
|
+
return (
|
|
112
|
+
<View style={styles.vContainer} accessibilityLabel={accessibilityLabel}>
|
|
113
|
+
{titleNode}
|
|
114
|
+
{/* the conversion rate is the headline — an aligned row across the top
|
|
115
|
+
(the funnel's story, read left→right); the first stage is the 100%
|
|
116
|
+
entry baseline. */}
|
|
117
|
+
<View style={styles.vRow}>
|
|
118
|
+
{stages.map((stage, i) => {
|
|
119
|
+
const rate = i === 0 ? formatRate(1) : rateOf(i);
|
|
120
|
+
return (
|
|
121
|
+
<View key={stage.key} style={styles.vCell}>
|
|
122
|
+
{rate ? (
|
|
123
|
+
<Text size="sm" weight="semibold" tabular>
|
|
124
|
+
{rate}
|
|
125
|
+
</Text>
|
|
126
|
+
) : null}
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</View>
|
|
131
|
+
<View style={[styles.vRow, { height: size }]}>
|
|
132
|
+
{stages.map((stage) => (
|
|
133
|
+
<View key={stage.key} style={styles.vSlot}>
|
|
134
|
+
{press(
|
|
135
|
+
stage.key,
|
|
136
|
+
`${stage.label}: ${formatValue(stage.value)}`,
|
|
137
|
+
true,
|
|
138
|
+
<View style={styles.vTrack}>
|
|
139
|
+
<View style={[styles.vFill, { height: `${fractionOf(stage.value) * 100}%`, backgroundColor: color, opacity: fillOpacity(stage.key) }]} />
|
|
140
|
+
</View>,
|
|
141
|
+
)}
|
|
142
|
+
</View>
|
|
143
|
+
))}
|
|
144
|
+
</View>
|
|
145
|
+
{/* count (supporting) + label below each column. */}
|
|
146
|
+
<View style={styles.vRow}>
|
|
147
|
+
{stages.map((stage) => (
|
|
148
|
+
<View key={stage.key} style={styles.vCaption}>
|
|
149
|
+
<Text size="sm" tabular>
|
|
150
|
+
{formatValue(stage.value)}
|
|
151
|
+
</Text>
|
|
152
|
+
<Text size="xs" color="muted" numberOfLines={1} style={styles.center}>
|
|
153
|
+
{stage.label}
|
|
154
|
+
</Text>
|
|
155
|
+
</View>
|
|
156
|
+
))}
|
|
157
|
+
</View>
|
|
158
|
+
</View>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<View style={styles.hContainer} accessibilityLabel={accessibilityLabel}>
|
|
164
|
+
{titleNode}
|
|
165
|
+
{stages.map((stage, i) => {
|
|
166
|
+
const rate = rateOf(i);
|
|
167
|
+
return (
|
|
168
|
+
<View key={stage.key}>
|
|
169
|
+
{press(
|
|
170
|
+
stage.key,
|
|
171
|
+
`${stage.label}: ${formatValue(stage.value)}`,
|
|
172
|
+
false,
|
|
173
|
+
<View style={styles.hStage}>
|
|
174
|
+
<View style={styles.hHead}>
|
|
175
|
+
<Text size="sm" weight="medium">
|
|
176
|
+
{stage.label}
|
|
177
|
+
</Text>
|
|
178
|
+
{rate ? (
|
|
179
|
+
<Text size="xs" color="muted" tabular>
|
|
180
|
+
{rate}
|
|
181
|
+
</Text>
|
|
182
|
+
) : null}
|
|
183
|
+
<View style={styles.spacer} />
|
|
184
|
+
<Text size="sm" weight="semibold" tabular>
|
|
185
|
+
{formatValue(stage.value)}
|
|
186
|
+
</Text>
|
|
187
|
+
</View>
|
|
188
|
+
<View style={[styles.hTrack, { height: size }]}>
|
|
189
|
+
<View style={[styles.hFill, { width: `${fractionOf(stage.value) * 100}%`, height: size, backgroundColor: color, opacity: fillOpacity(stage.key) }]} />
|
|
190
|
+
</View>
|
|
191
|
+
</View>,
|
|
192
|
+
)}
|
|
193
|
+
</View>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</View>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const styles = StyleSheet.create({
|
|
201
|
+
// vertical — columns fill the width evenly (flex:1), height ∝ value, capped at `size`
|
|
202
|
+
vContainer: { gap: 8 },
|
|
203
|
+
vRow: { flexDirection: "row", alignItems: "flex-end", gap: 10 },
|
|
204
|
+
vCell: { flex: 1, alignItems: "center" },
|
|
205
|
+
vSlot: { flex: 1, height: "100%" },
|
|
206
|
+
pressFill: { flex: 1, height: "100%" },
|
|
207
|
+
vTrack: { flex: 1, width: "100%", borderRadius: RADIUS, backgroundColor: colors.zinc[100], overflow: "hidden", justifyContent: "flex-end" },
|
|
208
|
+
vFill: { width: "100%", borderRadius: RADIUS },
|
|
209
|
+
vCaption: { flex: 1, alignItems: "center", gap: 1 },
|
|
210
|
+
center: { textAlign: "center" },
|
|
211
|
+
// horizontal — stacked rows, bar width ∝ value
|
|
212
|
+
hContainer: { gap: 12 },
|
|
213
|
+
hStage: { gap: 6 },
|
|
214
|
+
hHead: { flexDirection: "row", alignItems: "baseline", gap: 8 },
|
|
215
|
+
spacer: { flex: 1 },
|
|
216
|
+
hTrack: { borderRadius: RADIUS, backgroundColor: colors.zinc[100], overflow: "hidden" },
|
|
217
|
+
hFill: { borderRadius: RADIUS },
|
|
218
|
+
});
|
package/src/locale.tsx
CHANGED
|
@@ -24,12 +24,14 @@ export interface LoticsLocale {
|
|
|
24
24
|
/** `SortHeader` a11y prefix + asc/desc suffixes. */
|
|
25
25
|
sortHeader: Required<SortHeaderLabels>;
|
|
26
26
|
/** `OptionList` (and everything built on it — `Select`, `Combobox`, the
|
|
27
|
-
* in-cell editors): the select-all/deselect-all links, the empty state,
|
|
28
|
-
* the `Combobox` recents header. */
|
|
29
|
-
optionList: { selectAll: string; deselectAll: string; noResults: string; recent: string };
|
|
27
|
+
* in-cell editors): the select-all/deselect-all links, the empty state, the
|
|
28
|
+
* internal search-field placeholder, and the `Combobox` recents header. */
|
|
29
|
+
optionList: { selectAll: string; deselectAll: string; noResults: string; recent: string; searchPlaceholder: string };
|
|
30
30
|
/** `FilterChip` (and `ColumnFilter`): the generic clear affordance, used when
|
|
31
31
|
* a call site doesn't pass a dimension-specific `clearLabel`. */
|
|
32
32
|
filterChip: { clear: string };
|
|
33
|
+
/** `FormField`: the "Optional" marker shown next to the label when `optional`. */
|
|
34
|
+
formField: { optional: string };
|
|
33
35
|
/** `Drawer`: the record prev/next + close controls (screen-reader names). */
|
|
34
36
|
drawer: { previous: string; next: string; close: string };
|
|
35
37
|
/** `Confidence`: the full level phrase ("High confidence" …). */
|
|
@@ -74,8 +76,9 @@ export const en: LoticsLocale = {
|
|
|
74
76
|
ascending: ", ascending",
|
|
75
77
|
descending: ", descending",
|
|
76
78
|
},
|
|
77
|
-
optionList: { selectAll: "Select all", deselectAll: "Deselect all", noResults: "No results", recent: "Recent" },
|
|
79
|
+
optionList: { selectAll: "Select all", deselectAll: "Deselect all", noResults: "No results", recent: "Recent", searchPlaceholder: "Search…" },
|
|
78
80
|
filterChip: { clear: "Clear" },
|
|
81
|
+
formField: { optional: "Optional" },
|
|
79
82
|
drawer: { previous: "Previous record", next: "Next record", close: "Close" },
|
|
80
83
|
confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
|
|
81
84
|
remainderMeter: {
|
|
@@ -123,8 +126,9 @@ export const vi: LoticsLocale = {
|
|
|
123
126
|
ascending: " (tăng dần)",
|
|
124
127
|
descending: " (giảm dần)",
|
|
125
128
|
},
|
|
126
|
-
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", noResults: "Không có kết quả", recent: "Gần đây" },
|
|
129
|
+
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…" },
|
|
127
130
|
filterChip: { clear: "Xóa" },
|
|
131
|
+
formField: { optional: "Tùy chọn" },
|
|
128
132
|
drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
|
|
129
133
|
confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
|
|
130
134
|
remainderMeter: {
|
package/src/option_list.tsx
CHANGED
|
@@ -58,7 +58,7 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
58
58
|
icon="search"
|
|
59
59
|
value={list.query}
|
|
60
60
|
onChangeText={list.setQuery}
|
|
61
|
-
placeholder={search.placeholder ??
|
|
61
|
+
placeholder={search.placeholder ?? loc.searchPlaceholder}
|
|
62
62
|
autoFocus={!small}
|
|
63
63
|
autoCapitalize="none"
|
|
64
64
|
autoCorrect={false}
|