@lotics/ui 3.1.0 → 3.3.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/package.json +1 -1
- package/src/button.tsx +14 -1
- package/src/card_select_item.tsx +14 -6
- package/src/combobox.tsx +36 -5
- package/src/pagination.tsx +37 -7
- package/src/segmented_control.tsx +5 -1
- package/src/sort_header.tsx +24 -3
- package/src/table.tsx +5 -3
package/package.json
CHANGED
package/src/button.tsx
CHANGED
|
@@ -22,6 +22,10 @@ interface ButtonPropsBase {
|
|
|
22
22
|
icon?: IconName;
|
|
23
23
|
alignSelf?: "flex-start" | "flex-end" | "center" | "stretch" | "auto";
|
|
24
24
|
color?: ButtonColor;
|
|
25
|
+
/** Corner shape. `pill` (default) is the fully-rounded capsule — keep it for
|
|
26
|
+
* icon-only and toolbar buttons, and to preserve existing screens. `rounded`
|
|
27
|
+
* is a 10px radius — use it for labelled CTAs / action buttons. */
|
|
28
|
+
shape?: "pill" | "rounded";
|
|
25
29
|
loading?: boolean;
|
|
26
30
|
disabled?: boolean;
|
|
27
31
|
tooltip?: string | UseTooltipOptions;
|
|
@@ -49,6 +53,7 @@ export function Button(props: ButtonProps) {
|
|
|
49
53
|
alignSelf,
|
|
50
54
|
title,
|
|
51
55
|
color,
|
|
56
|
+
shape = "pill",
|
|
52
57
|
style,
|
|
53
58
|
disabled,
|
|
54
59
|
tooltip,
|
|
@@ -108,8 +113,11 @@ export function Button(props: ButtonProps) {
|
|
|
108
113
|
disabled={disabledOrLoading}
|
|
109
114
|
// @ts-ignore hovered is a react-native-web extension not in base RN types
|
|
110
115
|
style={({ pressed, hovered }) => {
|
|
116
|
+
const isPrimary = color === "primary" && !disabled;
|
|
117
|
+
const restPrimary = isPrimary && !pressed && !hovered;
|
|
111
118
|
return [
|
|
112
119
|
{
|
|
120
|
+
borderRadius: shape === "rounded" ? 10 : 999,
|
|
113
121
|
backgroundColor: disabled
|
|
114
122
|
? getButtonDisabledBackgroundColor(color)
|
|
115
123
|
: pressed
|
|
@@ -118,6 +126,12 @@ export function Button(props: ButtonProps) {
|
|
|
118
126
|
? getButtonHoverColor(color)
|
|
119
127
|
: getButtonBackgroundColor(color),
|
|
120
128
|
},
|
|
129
|
+
// Subtle depth on the primary: a soft drop shadow lifts it off the
|
|
130
|
+
// surface and a whisper of top highlight + a gentle vertical shade (the
|
|
131
|
+
// app-icon technique) catch the light — premium, not skeuomorphic. The
|
|
132
|
+
// lift stays on hover/press; only the gradient drops so the wash shows.
|
|
133
|
+
isPrimary && { boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.18)" },
|
|
134
|
+
restPrimary && { backgroundImage: `linear-gradient(180deg, ${colors.zinc["700"]} 0%, ${colors.zinc["900"]} 100%)` },
|
|
121
135
|
styles.button,
|
|
122
136
|
style,
|
|
123
137
|
alignSelf && { alignSelf },
|
|
@@ -228,7 +242,6 @@ const styles = StyleSheet.create({
|
|
|
228
242
|
justifyContent: "center",
|
|
229
243
|
height: 40,
|
|
230
244
|
paddingHorizontal: 10,
|
|
231
|
-
borderRadius: 999,
|
|
232
245
|
// react-native-web extensions
|
|
233
246
|
...({ touchAction: "manipulation", cursor: "pointer", transitionDuration: "0.1s", transitionProperty: "background-color" } as ViewStyle),
|
|
234
247
|
},
|
package/src/card_select_item.tsx
CHANGED
|
@@ -5,27 +5,35 @@ import { PressableHighlight } from "./pressable_highlight";
|
|
|
5
5
|
interface CardSelectItemProps {
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
onPress: () => void;
|
|
8
|
+
/** Persistent selection — when true, the ring stays on (the current item in a
|
|
9
|
+
* list with one active selection: an org picker's current org, a switcher's
|
|
10
|
+
* open item). Reads as `aria-pressed`. */
|
|
11
|
+
selected?: boolean;
|
|
8
12
|
testID?: string;
|
|
13
|
+
accessibilityLabel?: string;
|
|
9
14
|
style?: StyleProp<ViewStyle>;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* A bordered, card-shaped button — the selectable
|
|
14
|
-
* (organization picker, login choices)
|
|
15
|
-
* `button` that shows a 2px ring on hover/press,
|
|
16
|
-
* focus ring.
|
|
18
|
+
* A bordered, card-shaped button — the selectable item used on auth screens
|
|
19
|
+
* (organization picker, login choices) and in entity switchers. Always
|
|
20
|
+
* interactive: a real focusable `button` that shows a 2px ring on hover/press,
|
|
21
|
+
* matching the global keyboard focus ring. Pass `selected` to keep that ring on
|
|
22
|
+
* for the current item in a single-selection list. For a static surface use Card.
|
|
17
23
|
*/
|
|
18
24
|
export function CardSelectItem(props: CardSelectItemProps) {
|
|
19
|
-
const { children, onPress, testID, style } = props;
|
|
25
|
+
const { children, onPress, selected = false, testID, accessibilityLabel, style } = props;
|
|
20
26
|
|
|
21
27
|
return (
|
|
22
28
|
<PressableHighlight
|
|
23
29
|
testID={testID}
|
|
24
30
|
accessibilityRole="button"
|
|
31
|
+
accessibilityLabel={accessibilityLabel}
|
|
32
|
+
aria-pressed={selected}
|
|
25
33
|
onPress={onPress}
|
|
26
34
|
style={(state: PressableStateCallbackType) => {
|
|
27
35
|
const hovered = (state as { hovered?: boolean }).hovered;
|
|
28
|
-
const active = hovered || state.pressed;
|
|
36
|
+
const active = selected || hovered || state.pressed;
|
|
29
37
|
return [styles.container, active && styles.ring, style];
|
|
30
38
|
}}
|
|
31
39
|
>
|
package/src/combobox.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
11
11
|
import { colors } from "./colors";
|
|
12
12
|
import { Text } from "./text";
|
|
13
|
-
import { Icon } from "./icon";
|
|
13
|
+
import { Icon, type IconName } from "./icon";
|
|
14
14
|
import { TextInputField } from "./text_input_field";
|
|
15
15
|
import { MenuButton } from "./menu_button";
|
|
16
16
|
import { ActivityIndicator } from "./activity_indicator";
|
|
@@ -58,6 +58,11 @@ export interface ComboboxProps<T extends string = string, D = unknown> {
|
|
|
58
58
|
recentOptions?: PickerOption<T, D>[];
|
|
59
59
|
loading?: boolean;
|
|
60
60
|
searchDebounceMs?: number;
|
|
61
|
+
/** Leading icon inside the input. OMIT (the default) for a SELECT — no leading
|
|
62
|
+
* glyph, a trailing chevron — so a "pick a code/record" combobox reads as a
|
|
63
|
+
* picker, not a free-text search box. Opt IN to the search-box look explicitly
|
|
64
|
+
* with `icon="search"` (leading glyph, no chevron). Multi-select shows neither. */
|
|
65
|
+
icon?: IconName;
|
|
61
66
|
placeholder?: string;
|
|
62
67
|
recentsLabel?: string;
|
|
63
68
|
emptyText?: string;
|
|
@@ -109,6 +114,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
109
114
|
recentOptions,
|
|
110
115
|
loading = false,
|
|
111
116
|
searchDebounceMs = 200,
|
|
117
|
+
icon,
|
|
112
118
|
placeholder,
|
|
113
119
|
recentsLabel = "Recent",
|
|
114
120
|
emptyText = "No results",
|
|
@@ -157,11 +163,14 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
157
163
|
return options.filter((o) => (o.label ?? o.value).toLowerCase().includes(q));
|
|
158
164
|
}, [isServer, searching, options, multi, reflectSelection, single, query]);
|
|
159
165
|
|
|
160
|
-
// Free-entry row, appended when the query matches no option
|
|
166
|
+
// Free-entry row, appended when the query matches no option exactly. Match
|
|
167
|
+
// both label AND value — a query equal to an option's value (e.g. a code typed
|
|
168
|
+
// in full) must NOT also offer "Add <code>", or the two share a key.
|
|
161
169
|
const customRow = useMemo((): PickerOption<T, D> | null => {
|
|
162
170
|
if (!allowCustom || !searching) return null;
|
|
163
171
|
const q = query.trim();
|
|
164
|
-
const
|
|
172
|
+
const ql = q.toLowerCase();
|
|
173
|
+
const exact = options.some((o) => (o.label ?? o.value).toLowerCase() === ql || String(o.value).toLowerCase() === ql);
|
|
165
174
|
if (exact) return null;
|
|
166
175
|
const label = customOptionLabel ? customOptionLabel(q) : `Add "${q}"`;
|
|
167
176
|
if (label === null) return null;
|
|
@@ -257,6 +266,10 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
257
266
|
const showList = open && !disabled && (searching || list.length > 0);
|
|
258
267
|
const activeOptionId =
|
|
259
268
|
activeIndex >= 0 && activeIndex < list.length ? optionId(activeIndex) : undefined;
|
|
269
|
+
// Select mode (no leading icon): show a trailing chevron so the field reads as
|
|
270
|
+
// a picker, not a free-text search. The clear ✕ owns the right slot when
|
|
271
|
+
// present, so suppress the chevron then.
|
|
272
|
+
const showChevron = !multi && !icon && !clearable;
|
|
260
273
|
|
|
261
274
|
return (
|
|
262
275
|
<View style={style}>
|
|
@@ -281,7 +294,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
281
294
|
<TextInputField
|
|
282
295
|
ref={inputRef}
|
|
283
296
|
testID={testID}
|
|
284
|
-
icon={multi ? undefined :
|
|
297
|
+
icon={multi ? undefined : icon}
|
|
285
298
|
value={query}
|
|
286
299
|
clearable={!multi && clearable}
|
|
287
300
|
clearLabel={clearLabel}
|
|
@@ -308,13 +321,18 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
308
321
|
autoFocus={autoFocus}
|
|
309
322
|
autoCapitalize="none"
|
|
310
323
|
autoCorrect={false}
|
|
311
|
-
style={multi ? styles.multiInput : undefined}
|
|
324
|
+
style={multi ? styles.multiInput : showChevron ? styles.selectInput : undefined}
|
|
312
325
|
role="combobox"
|
|
313
326
|
aria-expanded={showList}
|
|
314
327
|
aria-controls={listboxId}
|
|
315
328
|
aria-activedescendant={activeOptionId}
|
|
316
329
|
aria-autocomplete="list"
|
|
317
330
|
/>
|
|
331
|
+
{showChevron ? (
|
|
332
|
+
<View style={styles.chevron}>
|
|
333
|
+
<Icon name="chevrons-up-down" size={16} color={colors.zinc["400"]} />
|
|
334
|
+
</View>
|
|
335
|
+
) : null}
|
|
318
336
|
</View>
|
|
319
337
|
<Popover
|
|
320
338
|
open={showList}
|
|
@@ -421,6 +439,19 @@ const styles = StyleSheet.create({
|
|
|
421
439
|
borderWidth: 0,
|
|
422
440
|
height: 28,
|
|
423
441
|
},
|
|
442
|
+
// Select mode: room on the right for the trailing chevron.
|
|
443
|
+
selectInput: {
|
|
444
|
+
paddingRight: 32,
|
|
445
|
+
},
|
|
446
|
+
chevron: {
|
|
447
|
+
position: "absolute",
|
|
448
|
+
right: 10,
|
|
449
|
+
top: 0,
|
|
450
|
+
bottom: 0,
|
|
451
|
+
justifyContent: "center",
|
|
452
|
+
pointerEvents: "none",
|
|
453
|
+
},
|
|
454
|
+
|
|
424
455
|
chip: {
|
|
425
456
|
flexDirection: "row",
|
|
426
457
|
alignItems: "center",
|
package/src/pagination.tsx
CHANGED
|
@@ -3,6 +3,34 @@ import { View, StyleSheet } from "react-native";
|
|
|
3
3
|
import { IconButton } from "./icon_button";
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Localizable strings for `Pagination`. Every field is optional; an omitted
|
|
8
|
+
* field keeps the English default, so an app that needs another language passes
|
|
9
|
+
* only what it overrides (mirrors `DateRangeFilterField`'s `labels`). The
|
|
10
|
+
* formatters receive 1-indexed display numbers and the raw total, so a caller
|
|
11
|
+
* controls its own locale (e.g. `total.toLocaleString("vi-VN")`).
|
|
12
|
+
*/
|
|
13
|
+
export interface PaginationLabels {
|
|
14
|
+
/** Range summary with a known total. Default `${start}–${end} of ${total}`. */
|
|
15
|
+
rangeWithTotal?: (start: number, end: number, total: number) => string;
|
|
16
|
+
/** Range summary without a total. Default `${start}–${end}`. */
|
|
17
|
+
range?: (start: number, end: number) => string;
|
|
18
|
+
/** Page indicator with a known total. Default `Page ${page} of ${pageCount}`. */
|
|
19
|
+
pageWithTotal?: (page: number, pageCount: number) => string;
|
|
20
|
+
/** Page indicator without a total. Default `Page ${page}`. */
|
|
21
|
+
page?: (page: number) => string;
|
|
22
|
+
/** Previous-page button tooltip. Default "Previous". */
|
|
23
|
+
previous?: string;
|
|
24
|
+
/** Next-page button tooltip. Default "Next". */
|
|
25
|
+
next?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultRangeWithTotal = (start: number, end: number, total: number): string =>
|
|
29
|
+
`${start}–${end} of ${total.toLocaleString()}`;
|
|
30
|
+
const defaultRange = (start: number, end: number): string => `${start}–${end}`;
|
|
31
|
+
const defaultPageWithTotal = (page: number, pageCount: number): string => `Page ${page} of ${pageCount}`;
|
|
32
|
+
const defaultPage = (page: number): string => `Page ${page}`;
|
|
33
|
+
|
|
6
34
|
export interface PaginationProps {
|
|
7
35
|
/** 0-indexed. */
|
|
8
36
|
page: number;
|
|
@@ -18,6 +46,8 @@ export interface PaginationProps {
|
|
|
18
46
|
total?: number;
|
|
19
47
|
loading?: boolean;
|
|
20
48
|
onPageChange: (page: number) => void;
|
|
49
|
+
/** Override the English strings for localization. Omit to keep defaults. */
|
|
50
|
+
labels?: PaginationLabels;
|
|
21
51
|
}
|
|
22
52
|
|
|
23
53
|
/**
|
|
@@ -28,21 +58,21 @@ export interface PaginationProps {
|
|
|
28
58
|
* doubling up a border.
|
|
29
59
|
*/
|
|
30
60
|
export function Pagination(props: PaginationProps): React.ReactNode {
|
|
31
|
-
const { page, pageSize, rowCount, hasMore, total, loading, onPageChange } = props;
|
|
61
|
+
const { page, pageSize, rowCount, hasMore, total, loading, onPageChange, labels } = props;
|
|
32
62
|
const start = page * pageSize + 1;
|
|
33
63
|
const end = page * pageSize + rowCount;
|
|
34
64
|
const showRange = !loading && rowCount > 0;
|
|
35
65
|
|
|
36
66
|
const summary = showRange
|
|
37
67
|
? total !== undefined
|
|
38
|
-
?
|
|
39
|
-
:
|
|
68
|
+
? (labels?.rangeWithTotal ?? defaultRangeWithTotal)(start, end, total)
|
|
69
|
+
: (labels?.range ?? defaultRange)(start, end)
|
|
40
70
|
: "";
|
|
41
71
|
|
|
42
72
|
const pageLabel =
|
|
43
73
|
total !== undefined
|
|
44
|
-
?
|
|
45
|
-
:
|
|
74
|
+
? (labels?.pageWithTotal ?? defaultPageWithTotal)(page + 1, Math.max(1, Math.ceil(total / pageSize)))
|
|
75
|
+
: (labels?.page ?? defaultPage)(page + 1);
|
|
46
76
|
|
|
47
77
|
return (
|
|
48
78
|
<View style={styles.container}>
|
|
@@ -60,14 +90,14 @@ export function Pagination(props: PaginationProps): React.ReactNode {
|
|
|
60
90
|
color="secondary"
|
|
61
91
|
onPress={() => onPageChange(Math.max(0, page - 1))}
|
|
62
92
|
disabled={page === 0 || !!loading}
|
|
63
|
-
tooltip="Previous"
|
|
93
|
+
tooltip={labels?.previous ?? "Previous"}
|
|
64
94
|
/>
|
|
65
95
|
<IconButton
|
|
66
96
|
icon="chevron-right"
|
|
67
97
|
color="secondary"
|
|
68
98
|
onPress={() => onPageChange(page + 1)}
|
|
69
99
|
disabled={!hasMore || !!loading}
|
|
70
|
-
tooltip="Next"
|
|
100
|
+
tooltip={labels?.next ?? "Next"}
|
|
71
101
|
/>
|
|
72
102
|
</View>
|
|
73
103
|
</View>
|
|
@@ -186,7 +186,11 @@ const styles = StyleSheet.create({
|
|
|
186
186
|
opacity: 0.55,
|
|
187
187
|
},
|
|
188
188
|
segment: {
|
|
189
|
-
|
|
189
|
+
// Grow to share a stretched track equally, but keep an `auto` basis so a
|
|
190
|
+
// hug-content track (the common toolbar case) sizes each segment to its
|
|
191
|
+
// label instead of collapsing it (flex-basis 0 → truncated text).
|
|
192
|
+
flexGrow: 1,
|
|
193
|
+
flexBasis: "auto",
|
|
190
194
|
minHeight: 30,
|
|
191
195
|
paddingVertical: 6,
|
|
192
196
|
paddingHorizontal: 14,
|
package/src/sort_header.tsx
CHANGED
|
@@ -39,6 +39,20 @@ export function sortBy<T>(
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Localizable a11y fragments for `SortHeader`. The visible header is the app's
|
|
44
|
+
* own `label`; only the screen-reader announcement is built here, so these
|
|
45
|
+
* cover that string. Omit any field to keep the English default.
|
|
46
|
+
*/
|
|
47
|
+
export interface SortHeaderLabels {
|
|
48
|
+
/** a11y prefix around the column label. Default `(label) => `Sort by ${label}``. */
|
|
49
|
+
sortBy?: (label: string) => string;
|
|
50
|
+
/** Appended when sorted ascending. Default ", ascending". */
|
|
51
|
+
ascending?: string;
|
|
52
|
+
/** Appended when sorted descending. Default ", descending". */
|
|
53
|
+
descending?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
export interface SortHeaderProps {
|
|
43
57
|
label: string;
|
|
44
58
|
sortKey: string;
|
|
@@ -47,6 +61,8 @@ export interface SortHeaderProps {
|
|
|
47
61
|
/** Right-align for numeric columns — the arrow then sits LEFT of the label. */
|
|
48
62
|
align?: "left" | "right";
|
|
49
63
|
style?: ViewStyle;
|
|
64
|
+
/** Override the English a11y strings for localization. Omit to keep defaults. */
|
|
65
|
+
labels?: SortHeaderLabels;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
/**
|
|
@@ -57,15 +73,20 @@ export interface SortHeaderProps {
|
|
|
57
73
|
* flush with the column content beneath it.
|
|
58
74
|
*/
|
|
59
75
|
export function SortHeader(props: SortHeaderProps) {
|
|
60
|
-
const { label, sortKey, sort, onSort, align = "left", style } = props;
|
|
76
|
+
const { label, sortKey, sort, onSort, align = "left", style, labels } = props;
|
|
61
77
|
const active = sort?.key === sortKey;
|
|
62
78
|
const arrow = active ? (sort.dir === "asc" ? "chevron-up" : "chevron-down") : undefined;
|
|
63
|
-
const dirText = active
|
|
79
|
+
const dirText = active
|
|
80
|
+
? sort.dir === "asc"
|
|
81
|
+
? (labels?.ascending ?? ", ascending")
|
|
82
|
+
: (labels?.descending ?? ", descending")
|
|
83
|
+
: "";
|
|
84
|
+
const sortByLabel = (labels?.sortBy ?? ((l: string) => `Sort by ${l}`))(label);
|
|
64
85
|
|
|
65
86
|
return (
|
|
66
87
|
<PressableHighlight
|
|
67
88
|
accessibilityRole="button"
|
|
68
|
-
accessibilityLabel={
|
|
89
|
+
accessibilityLabel={`${sortByLabel}${dirText}`}
|
|
69
90
|
onPress={() => onSort(sortKey)}
|
|
70
91
|
style={[styles.header, align === "right" ? styles.right : null, style]}
|
|
71
92
|
>
|
package/src/table.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { StyleSheet, View, Pressable, type ViewStyle } from "react-native";
|
|
|
11
11
|
import { Text } from "./text";
|
|
12
12
|
import { Divider } from "./divider";
|
|
13
13
|
import { PressableRow } from "./pressable_row";
|
|
14
|
-
import { SortHeader, type SortState } from "./sort_header";
|
|
14
|
+
import { SortHeader, type SortState, type SortHeaderLabels } from "./sort_header";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* One column of a register — its width/flex/align/label/sortability defined ONCE,
|
|
@@ -50,6 +50,8 @@ export interface TableProps {
|
|
|
50
50
|
sort?: SortState | null;
|
|
51
51
|
/** Cycles the sort (none→asc→desc→none) — pair with `cycleSort` in the parent. */
|
|
52
52
|
onSort?: (key: string) => void;
|
|
53
|
+
/** Localized a11y strings for the sortable headers. Omit to keep English. */
|
|
54
|
+
sortLabels?: SortHeaderLabels;
|
|
53
55
|
/** Reserve a leading gutter (px) for rows that render a `leading` slot (a checkbox). */
|
|
54
56
|
leading?: number;
|
|
55
57
|
/** Reserve a trailing gutter (px) for rows that render a `trailing` slot (a ⋯ / button). */
|
|
@@ -67,7 +69,7 @@ export interface TableProps {
|
|
|
67
69
|
* non-columnar list (entity piles, card stacks) use `PressableRow` directly.
|
|
68
70
|
*/
|
|
69
71
|
export function Table(props: TableProps) {
|
|
70
|
-
const { columns, sort, onSort, leading = 0, trailing = 0, children } = props;
|
|
72
|
+
const { columns, sort, onSort, sortLabels, leading = 0, trailing = 0, children } = props;
|
|
71
73
|
const rows = Children.toArray(children).filter(isValidElement);
|
|
72
74
|
|
|
73
75
|
return (
|
|
@@ -78,7 +80,7 @@ export function Table(props: TableProps) {
|
|
|
78
80
|
<View key={col.key} style={colStyle(col)}>
|
|
79
81
|
{col.label ? (
|
|
80
82
|
col.sortable && onSort ? (
|
|
81
|
-
<SortHeader label={col.label} sortKey={col.key} sort={sort ?? null} onSort={onSort} align={col.align} />
|
|
83
|
+
<SortHeader label={col.label} sortKey={col.key} sort={sort ?? null} onSort={onSort} align={col.align} labels={sortLabels} />
|
|
82
84
|
) : (
|
|
83
85
|
<Text size="xs" color="muted" transform="uppercase" numberOfLines={1}>
|
|
84
86
|
{col.label}
|