@lotics/ui 6.1.0 → 6.2.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 CHANGED
@@ -19,10 +19,11 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
19
19
  redefine — or drift on — strings). Resolution is **prop → provider locale → English default**, so a
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
- (+ `Select`/`Combobox` built on it), `FilterChip`, `Drawer`. Adding a string to a wired component's
23
- `*Labels` forces both packs in `locale.tsx` to fill it (compile error) that's how the kit avoids a
24
- silent English leak. Not yet on the provider (pass `labels` for now): `DateRangeFilterField`,
25
- `RemainderMeter`, `ChangeReview`, calendar/gantt.
22
+ (+ `Select`/`Combobox` built on it), `FilterChip`, `Drawer`, `Confidence`, `RemainderMeter`,
23
+ `ChangeReview`/`ChangeReviewActions`, and `DateRangeFilterField`/`DateFilter` (presets + footer +
24
+ time-segment a11y, one `dateRange` slice). Adding a string to a wired component's `*Labels` forces
25
+ both packs in `locale.tsx` to fill it (compile error) — that's how the kit avoids a silent English
26
+ leak. Not yet on the provider (pass `labels` for now): calendar/gantt, the file/preview/comment labels.
26
27
 
27
28
  ---
28
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -5,6 +5,7 @@ import { reviewCardStyle, reviewResolvedStyle } from "./control_surface";
5
5
  import { Text } from "./text";
6
6
  import { Icon } from "./icon";
7
7
  import { Button } from "./button";
8
+ import { useLoticsLocale } from "./locale";
8
9
 
9
10
  export type ChangeReviewItemStatus = "pending" | "accepted" | "rejected";
10
11
 
@@ -116,17 +117,18 @@ export function ChangeDiff({ before, after }: { before?: string; after: string }
116
117
  * `ReviewCard` reviews a SINGLE proposal, this is the BATCH engine.
117
118
  */
118
119
  export function ChangeReview<T>(props: ChangeReviewProps<T>) {
120
+ const loc = useLoticsLocale().changeReview;
119
121
  const status = props.status ?? "open";
120
122
  const perItem = props.onAcceptItem != null || props.onRejectItem != null;
121
123
  const total = props.items.length;
122
124
  const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
123
125
  const keptCount = props.items.filter((it, i) => statusFor(it, i) === "accepted").length;
124
- const title = props.title ?? "Suggested edits";
126
+ const title = props.title ?? loc.title;
125
127
  const L = {
126
- undo: "Undo",
127
- applied: "Applied",
128
- discarded: "Discarded",
129
- keptCount: (kept: number, total: number) => `${kept} of ${total} kept`,
128
+ undo: loc.undo,
129
+ applied: loc.applied,
130
+ discarded: loc.discarded,
131
+ keptCount: loc.keptCount,
130
132
  ...props.labels,
131
133
  };
132
134
 
@@ -173,8 +175,8 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
173
175
  <View key={props.getKey(item, i)} style={styles.itemCard}>
174
176
  {props.renderItem(item, i)}
175
177
  <View style={styles.itemActions}>
176
- <Button title={props.rejectLabel ?? "Drop"} color="muted" onPress={() => props.onRejectItem?.(i)} />
177
- <Button title={props.acceptLabel ?? "Keep"} color="secondary" onPress={() => props.onAcceptItem?.(i)} />
178
+ <Button title={props.rejectLabel ?? loc.reject} color="muted" onPress={() => props.onRejectItem?.(i)} />
179
+ <Button title={props.acceptLabel ?? loc.accept} color="secondary" onPress={() => props.onAcceptItem?.(i)} />
178
180
  </View>
179
181
  </View>
180
182
  );
@@ -233,6 +235,7 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
233
235
  * inline wrapper provides the chrome); self-hides once `status` isn't open.
234
236
  */
235
237
  export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
238
+ const loc = useLoticsLocale().changeReview;
236
239
  if ((props.status ?? "open") !== "open") return null;
237
240
  const perItem = props.onAcceptItem != null;
238
241
  const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
@@ -243,12 +246,12 @@ export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
243
246
  });
244
247
  return (
245
248
  <View style={styles.actionsRow}>
246
- {perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted" onPress={acceptAll} /> : null}
249
+ {perItem ? <Button title={props.acceptAllLabel ?? loc.acceptAll} color="muted" onPress={acceptAll} /> : null}
247
250
  <View style={{ flex: 1 }} />
248
- {props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted" onPress={props.onDiscard} /> : null}
251
+ {props.onDiscard ? <Button title={props.discardLabel ?? loc.discard} color="muted" onPress={props.onDiscard} /> : null}
249
252
  {props.onApply ? (
250
253
  <Button
251
- title={props.applyLabel ?? (perItem ? "Apply kept" : "Apply")}
254
+ title={props.applyLabel ?? (perItem ? loc.applyKept : loc.apply)}
252
255
  color="primary"
253
256
  disabled={perItem && keptCount === 0}
254
257
  onPress={props.onApply}
@@ -1,6 +1,7 @@
1
1
  import { StyleSheet, View } from "react-native";
2
2
  import { colors, solid, type ColorName } from "./colors";
3
3
  import { Text } from "./text";
4
+ import { useLoticsLocale } from "./locale";
4
5
 
5
6
  export type ConfidenceLevel = "high" | "medium" | "low";
6
7
 
@@ -20,7 +21,6 @@ export interface ConfidenceProps {
20
21
  labels?: Partial<ConfidenceLabels>;
21
22
  }
22
23
 
23
- const DEFAULT_LABELS: ConfidenceLabels = { high: "High confidence", medium: "Medium confidence", low: "Low confidence" };
24
24
  const FILLED: Record<ConfidenceLevel, number> = { high: 3, medium: 2, low: 1 };
25
25
  // high emerald, medium amber, low zinc (unsure — not an error). One family per
26
26
  // level; the fill count AND the colour both carry it.
@@ -40,7 +40,7 @@ export function levelFromScore(score: number): ConfidenceLevel {
40
40
  */
41
41
  export function Confidence(props: ConfidenceProps) {
42
42
  const level = props.level ?? (props.score != null ? levelFromScore(props.score) : "medium");
43
- const l = { ...DEFAULT_LABELS, ...props.labels };
43
+ const l = { ...useLoticsLocale().confidence, ...props.labels };
44
44
  const filled = FILLED[level];
45
45
  const fill = solid(COLOR[level]);
46
46
  return (
@@ -11,6 +11,7 @@ import { useScreenSize } from "./use_screen_size";
11
11
  import { SegmentLabels } from "./date_segments";
12
12
  import { PresetId, PRESET_IDS, getPresetValue } from "./date_filter_presets";
13
13
  import { formatDate } from "./format_date";
14
+ import { useLoticsLocale } from "./locale";
14
15
 
15
16
  type SelectionMode = "single" | "range";
16
17
 
@@ -42,25 +43,6 @@ export interface DateFilterLabels extends SegmentLabels {
42
43
  selectDate: string;
43
44
  }
44
45
 
45
- const DEFAULT_LABELS: DateFilterLabels = {
46
- year: "Year",
47
- month: "Month",
48
- day: "Day",
49
- hour: "Hour",
50
- minute: "Minute",
51
- dayPeriod: "AM/PM",
52
- today: "Today",
53
- yesterday: "Yesterday",
54
- tomorrow: "Tomorrow",
55
- thisWeek: "This week",
56
- thisMonth: "This month",
57
- lastMonth: "Last month",
58
- custom: "Custom",
59
- from: "From",
60
- to: "To",
61
- selectDateRange: "Select date range",
62
- selectDate: "Select date",
63
- };
64
46
 
65
47
  export interface DateFilterProps {
66
48
  value: DateFilterValue;
@@ -106,9 +88,10 @@ function formatDateDisplay(date: Date | null, locale: string | undefined): strin
106
88
 
107
89
  export function DateFilter(props: DateFilterProps) {
108
90
  const { value, onValueChange, includeTime = false, locale } = props;
91
+ const loc = useLoticsLocale().dateRange;
109
92
  const labels = useMemo<DateFilterLabels>(
110
- () => ({ ...DEFAULT_LABELS, ...props.labels }),
111
- [props.labels],
93
+ () => ({ ...loc, ...props.labels }),
94
+ [loc, props.labels],
112
95
  );
113
96
  const screenSize = useScreenSize();
114
97
  const calendarRef = useRef<CalendarRef>(null);
@@ -9,6 +9,7 @@ import { PressableHighlight } from "./pressable_highlight";
9
9
  import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
10
10
  import { DateFilter, DateFilterValue, DateFilterLabels } from "./date_filter";
11
11
  import { formatDate } from "./format_date";
12
+ import { useLoticsLocale } from "./locale";
12
13
 
13
14
  // =============================================================================
14
15
  // DateRangeFilterField — the common filter composition over DateFilter:
@@ -27,7 +28,6 @@ export interface DateRangeFilterFieldLabels extends DateFilterLabels {
27
28
  placeholder: string;
28
29
  }
29
30
 
30
- const DEFAULT_FIELD_LABELS = { clear: "Clear", done: "Done", placeholder: "All time" };
31
31
 
32
32
  export interface DateRangeFilterFieldProps {
33
33
  value: DateFilterValue;
@@ -110,7 +110,8 @@ function formatTrigger(value: DateFilterValue, includeTime: boolean, locale: str
110
110
 
111
111
  export function DateRangeFilterField(props: DateRangeFilterFieldProps) {
112
112
  const { value, onValueChange, includeTime, locale, testID } = props;
113
- const labels = useMemo(() => ({ ...DEFAULT_FIELD_LABELS, ...props.labels }), [props.labels]);
113
+ const loc = useLoticsLocale().dateRange;
114
+ const labels = useMemo(() => ({ ...loc, ...props.labels }), [loc, props.labels]);
114
115
  const [open, setOpen] = useState(false);
115
116
 
116
117
  const hasValue = Boolean(value.start.date || value.end.date);
package/src/locale.tsx CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createContext, useContext, type ReactNode } from "react";
2
2
  import { type PaginationLabels } from "./pagination";
3
3
  import { type SortHeaderLabels } from "./sort_header";
4
+ import { type ConfidenceLabels } from "./confidence";
5
+ import { type RemainderMeterLabels } from "./remainder_meter";
6
+ import { type DateRangeFilterFieldLabels } from "./date_range_filter_field";
4
7
 
5
8
  /**
6
9
  * The kit's localizable strings, one slice per string-bearing component. A
@@ -29,6 +32,30 @@ export interface LoticsLocale {
29
32
  filterChip: { clear: string };
30
33
  /** `Drawer`: the record prev/next + close controls (screen-reader names). */
31
34
  drawer: { previous: string; next: string; close: string };
35
+ /** `Confidence`: the full level phrase ("High confidence" …). */
36
+ confidence: ConfidenceLabels;
37
+ /** `RemainderMeter`: the applied / remaining / over / exact captions. */
38
+ remainderMeter: Required<RemainderMeterLabels>;
39
+ /** `ChangeReview` (+ `ChangeReviewActions`): the review-card chrome — the
40
+ * Keep/Drop verdicts, Undo, the Applied/Discarded tags, the kept-counter,
41
+ * the title, and the commit-bar Accept-all / Discard. */
42
+ changeReview: {
43
+ title: string;
44
+ accept: string;
45
+ reject: string;
46
+ undo: string;
47
+ applied: string;
48
+ discarded: string;
49
+ keptCount: (kept: number, total: number) => string;
50
+ acceptAll: string;
51
+ discard: string;
52
+ apply: string;
53
+ applyKept: string;
54
+ };
55
+ /** `DateRangeFilterField` (and the `DateFilter` panel it wraps): presets,
56
+ * from/to, the footer Clear/Done, the trigger placeholder, and the time-field
57
+ * segment a11y names. */
58
+ dateRange: DateRangeFilterFieldLabels;
32
59
  }
33
60
 
34
61
  /** The platform default — English. Every component's hardcoded default lives
@@ -50,6 +77,34 @@ export const en: LoticsLocale = {
50
77
  optionList: { selectAll: "Select all", deselectAll: "Deselect all", noResults: "No results", recent: "Recent" },
51
78
  filterChip: { clear: "Clear" },
52
79
  drawer: { previous: "Previous record", next: "Next record", close: "Close" },
80
+ confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
81
+ remainderMeter: {
82
+ applied: (allocated, total) => `${allocated} of ${total} applied`,
83
+ remaining: (remainder) => `${remainder} unapplied`,
84
+ over: (amount) => `Over by ${amount}`,
85
+ exact: "Fully applied",
86
+ },
87
+ changeReview: {
88
+ title: "Suggested edits",
89
+ accept: "Keep",
90
+ reject: "Drop",
91
+ undo: "Undo",
92
+ applied: "Applied",
93
+ discarded: "Discarded",
94
+ keptCount: (kept, total) => `${kept} of ${total} kept`,
95
+ acceptAll: "Accept all",
96
+ discard: "Discard",
97
+ apply: "Apply",
98
+ applyKept: "Apply kept",
99
+ },
100
+ dateRange: {
101
+ year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
102
+ today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow",
103
+ thisWeek: "This week", thisMonth: "This month", lastMonth: "Last month",
104
+ custom: "Custom", from: "From", to: "To",
105
+ selectDateRange: "Select date range", selectDate: "Select date",
106
+ clear: "Clear", done: "Done", placeholder: "All time",
107
+ },
53
108
  };
54
109
 
55
110
  /** Vietnamese. Maintained once here so every app (and the frontend) shares one
@@ -71,6 +126,34 @@ export const vi: LoticsLocale = {
71
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" },
72
127
  filterChip: { clear: "Xóa" },
73
128
  drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
129
+ confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
130
+ remainderMeter: {
131
+ applied: (allocated, total) => `Đã phân bổ ${allocated}/${total}`,
132
+ remaining: (remainder) => `Còn ${remainder}`,
133
+ over: (amount) => `Vượt ${amount}`,
134
+ exact: "Đã phân bổ đủ",
135
+ },
136
+ changeReview: {
137
+ title: "Đề xuất chỉnh sửa",
138
+ accept: "Giữ",
139
+ reject: "Bỏ",
140
+ undo: "Hoàn tác",
141
+ applied: "Đã áp dụng",
142
+ discarded: "Đã bỏ",
143
+ keptCount: (kept, total) => `Giữ ${kept}/${total}`,
144
+ acceptAll: "Giữ tất cả",
145
+ discard: "Hủy",
146
+ apply: "Áp dụng",
147
+ applyKept: "Áp dụng mục đã giữ",
148
+ },
149
+ dateRange: {
150
+ year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
151
+ today: "Hôm nay", yesterday: "Hôm qua", tomorrow: "Ngày mai",
152
+ thisWeek: "Tuần này", thisMonth: "Tháng này", lastMonth: "Tháng trước",
153
+ custom: "Tùy chọn", from: "Từ", to: "Đến",
154
+ selectDateRange: "Chọn khoảng ngày", selectDate: "Chọn ngày",
155
+ clear: "Xóa", done: "Xong", placeholder: "Tất cả thời gian",
156
+ },
74
157
  };
75
158
 
76
159
  const LoticsLocaleContext = createContext<LoticsLocale>(en);
@@ -1,6 +1,7 @@
1
1
  import { StyleSheet, View, type DimensionValue } from "react-native";
2
2
  import { colors, solid } from "./colors";
3
3
  import { Text } from "./text";
4
+ import { useLoticsLocale } from "./locale";
4
5
 
5
6
  /**
6
7
  * Override the meter's captions for localization. Each key is optional and
@@ -17,11 +18,6 @@ export interface RemainderMeterLabels {
17
18
  exact?: string;
18
19
  }
19
20
 
20
- const defaultApplied = (allocated: string, total: string) => `${allocated} of ${total} applied`;
21
- const defaultRemaining = (remainder: string) => `${remainder} unapplied`;
22
- const defaultOver = (amount: string) => `Over by ${amount}`;
23
- const DEFAULT_EXACT = "Fully applied";
24
-
25
21
  export interface RemainderMeterProps {
26
22
  /** The source amount being distributed — the payment, the available stock. */
27
23
  total: number;
@@ -43,20 +39,21 @@ export interface RemainderMeterProps {
43
39
  */
44
40
  export function RemainderMeter(props: RemainderMeterProps) {
45
41
  const { total, allocated, format = (n) => n.toLocaleString(), labels } = props;
42
+ const loc = useLoticsLocale().remainderMeter;
46
43
  const remainder = total - allocated;
47
44
  const state = remainder > 0 ? "under" : remainder < 0 ? "over" : "exact";
48
45
  const pct: DimensionValue = `${total <= 0 ? 0 : Math.min(100, (allocated / total) * 100)}%`;
49
46
  const barColor = state === "over" ? solid("red") : state === "exact" ? solid("emerald") : solid("blue");
50
47
  const right =
51
48
  state === "exact"
52
- ? (labels?.exact ?? DEFAULT_EXACT)
49
+ ? (labels?.exact ?? loc.exact)
53
50
  : state === "over"
54
- ? (labels?.over ?? defaultOver)(format(-remainder))
55
- : (labels?.remaining ?? defaultRemaining)(format(remainder));
51
+ ? (labels?.over ?? loc.over)(format(-remainder))
52
+ : (labels?.remaining ?? loc.remaining)(format(remainder));
56
53
  return (
57
54
  <View style={{ gap: 8 }}>
58
55
  <View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
59
- <Text size="sm" color="muted" style={{ flex: 1 }}>{(labels?.applied ?? defaultApplied)(format(allocated), format(total))}</Text>
56
+ <Text size="sm" color="muted" style={{ flex: 1 }}>{(labels?.applied ?? loc.applied)(format(allocated), format(total))}</Text>
60
57
  <Text size="sm" weight="medium" tabular color={state === "over" ? "danger" : state === "exact" ? "success" : "default"}>
61
58
  {right}
62
59
  </Text>