@lotics/ui 1.14.0 → 1.16.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 +16 -1
- package/src/combobox.tsx +13 -1
- package/src/custom_option.test.ts +50 -0
- package/src/custom_option.ts +30 -0
- package/src/file_gallery_modal.tsx +22 -40
- package/src/file_preview.tsx +38 -0
- package/src/file_preview.web.tsx +198 -0
- package/src/file_preview_types.ts +35 -0
- package/src/menu_list_item.tsx +43 -3
- package/src/mime.ts +46 -0
- package/src/picker_menu.tsx +29 -5
- package/src/search_select.tsx +47 -47
- package/src/spreadsheet_view.tsx +304 -0
- package/src/table.tsx +105 -36
package/src/table.tsx
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
|
-
import { ScrollView, View, ViewStyle, StyleSheet } from "react-native";
|
|
1
|
+
import { Pressable, ScrollView, View, ViewStyle, StyleSheet } from "react-native";
|
|
2
2
|
import { Text } from "@lotics/ui/text";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import { colors } from "@lotics/ui/colors";
|
|
5
5
|
|
|
6
|
+
export type SortDir = "asc" | "desc";
|
|
7
|
+
export interface TableSort<TRow extends Record<string, unknown>> {
|
|
8
|
+
key: keyof TRow;
|
|
9
|
+
dir: SortDir;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
interface TableProps<TRow extends Record<string, unknown>> {
|
|
7
13
|
columns: Column<TRow>[];
|
|
8
14
|
rows: (TRow | null | false)[];
|
|
9
15
|
rowKey?: keyof TRow;
|
|
10
16
|
rowStyle?: (row: TRow) => ViewStyle | undefined;
|
|
17
|
+
/** Current sort. Pair with `onSortChange` to render sortable headers. The
|
|
18
|
+
* parent owns the actual sorting of `rows` — this only drives the indicator. */
|
|
19
|
+
sort?: TableSort<TRow> | null;
|
|
20
|
+
/** Pressing a `sortable` header calls this with the column key (the parent
|
|
21
|
+
* toggles asc/desc and re-sorts `rows`). */
|
|
22
|
+
onSortChange?: (key: keyof TRow) => void;
|
|
23
|
+
/** Pressing a row calls this — e.g. to open a detail drilldown. The row
|
|
24
|
+
* becomes a Pressable with a hover/press surface. Interactive cells nested
|
|
25
|
+
* inside (Pressables/buttons) capture their own press and don't bubble. */
|
|
26
|
+
onRowPress?: (row: TRow) => void;
|
|
11
27
|
}
|
|
12
28
|
|
|
13
29
|
interface Column<TRow extends Record<string, unknown>> {
|
|
14
30
|
key: keyof TRow;
|
|
15
31
|
label: string;
|
|
16
32
|
width?: number;
|
|
33
|
+
/** Horizontal alignment of header + cell content. Default "left". */
|
|
34
|
+
align?: "left" | "right";
|
|
35
|
+
/** When true (and `onSortChange` is set), the header is pressable + shows a
|
|
36
|
+
* sort arrow when this column is the active `sort`. */
|
|
37
|
+
sortable?: boolean;
|
|
17
38
|
renderCell?: RenderCell<TRow>;
|
|
18
39
|
}
|
|
19
40
|
|
|
@@ -25,48 +46,80 @@ type RenderCell<TRow extends Record<string, unknown>> = (params: {
|
|
|
25
46
|
const stickyHeader = [0];
|
|
26
47
|
|
|
27
48
|
export function Table<TRow extends Record<string, unknown>>(props: TableProps<TRow>) {
|
|
28
|
-
const { columns, rows, rowKey, rowStyle } = props;
|
|
49
|
+
const { columns, rows, rowKey, rowStyle, sort, onSortChange, onRowPress } = props;
|
|
29
50
|
|
|
30
51
|
return (
|
|
31
52
|
<ScrollView style={styles.container} stickyHeaderIndices={stickyHeader}>
|
|
32
53
|
<View style={styles.headerRow}>
|
|
33
|
-
{columns.map((column) =>
|
|
54
|
+
{columns.map((column) => {
|
|
55
|
+
const sortable = column.sortable && !!onSortChange;
|
|
56
|
+
const active = sort?.key === column.key;
|
|
57
|
+
const cellStyle = [
|
|
58
|
+
column.width ? { width: column.width } : styles.flexColumn,
|
|
59
|
+
styles.headerCell,
|
|
60
|
+
column.align === "right" ? styles.alignEnd : null,
|
|
61
|
+
];
|
|
62
|
+
const inner = (
|
|
63
|
+
<View style={styles.headerInner}>
|
|
64
|
+
<Text weight="medium" numberOfLines={1} userSelect="none">
|
|
65
|
+
{column.label}
|
|
66
|
+
</Text>
|
|
67
|
+
{sortable ? (
|
|
68
|
+
<Text size="xs" color="muted" userSelect="none">
|
|
69
|
+
{active ? (sort?.dir === "asc" ? "↑" : "↓") : "↕"}
|
|
70
|
+
</Text>
|
|
71
|
+
) : null}
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
return sortable ? (
|
|
75
|
+
<Pressable
|
|
76
|
+
key={column.key as string}
|
|
77
|
+
accessibilityRole="button"
|
|
78
|
+
accessibilityLabel={`Sắp xếp theo ${column.label}`}
|
|
79
|
+
onPress={() => onSortChange?.(column.key)}
|
|
80
|
+
style={cellStyle}
|
|
81
|
+
>
|
|
82
|
+
{inner}
|
|
83
|
+
</Pressable>
|
|
84
|
+
) : (
|
|
85
|
+
<View key={column.key as string} style={cellStyle}>
|
|
86
|
+
{inner}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</View>
|
|
91
|
+
{rows.filter(Boolean).map((r, i) => {
|
|
92
|
+
const row = r as TRow;
|
|
93
|
+
const extraStyle = rowStyle?.(row);
|
|
94
|
+
const cells = columns.map((column) => (
|
|
34
95
|
<View
|
|
96
|
+
key={column.key as string}
|
|
35
97
|
style={[
|
|
36
98
|
column.width ? { width: column.width } : styles.flexColumn,
|
|
37
|
-
styles.
|
|
99
|
+
styles.bodyCell,
|
|
100
|
+
column.align === "right" ? styles.alignEnd : null,
|
|
38
101
|
]}
|
|
39
|
-
key={column.key as string}
|
|
40
102
|
>
|
|
41
|
-
|
|
42
|
-
{column
|
|
43
|
-
|
|
103
|
+
{column.renderCell ? (
|
|
104
|
+
column.renderCell({ row, column })
|
|
105
|
+
) : (
|
|
106
|
+
<Text numberOfLines={1}>{row[column.key]?.toString()}</Text>
|
|
107
|
+
)}
|
|
44
108
|
</View>
|
|
45
|
-
))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
style={[styles.bodyRow, extraStyle]}
|
|
109
|
+
));
|
|
110
|
+
const key = rowKey ? String(row[rowKey]) : i;
|
|
111
|
+
return onRowPress ? (
|
|
112
|
+
<Pressable
|
|
113
|
+
key={key}
|
|
114
|
+
accessibilityRole="button"
|
|
115
|
+
onPress={() => onRowPress(row)}
|
|
116
|
+
style={({ pressed }) => [styles.bodyRow, pressed ? styles.rowPressed : null, extraStyle]}
|
|
54
117
|
>
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
styles.bodyCell,
|
|
61
|
-
]}
|
|
62
|
-
>
|
|
63
|
-
{column.renderCell ? (
|
|
64
|
-
column.renderCell({ row, column })
|
|
65
|
-
) : (
|
|
66
|
-
<Text numberOfLines={1}>{row[column.key]?.toString()}</Text>
|
|
67
|
-
)}
|
|
68
|
-
</View>
|
|
69
|
-
))}
|
|
118
|
+
{cells}
|
|
119
|
+
</Pressable>
|
|
120
|
+
) : (
|
|
121
|
+
<View key={key} style={[styles.bodyRow, extraStyle]}>
|
|
122
|
+
{cells}
|
|
70
123
|
</View>
|
|
71
124
|
);
|
|
72
125
|
})}
|
|
@@ -86,19 +139,35 @@ const styles = StyleSheet.create({
|
|
|
86
139
|
backgroundColor: colors.zinc[50],
|
|
87
140
|
},
|
|
88
141
|
headerCell: {
|
|
89
|
-
|
|
142
|
+
minHeight: 40,
|
|
90
143
|
justifyContent: "center",
|
|
91
|
-
paddingHorizontal:
|
|
144
|
+
paddingHorizontal: 12,
|
|
145
|
+
paddingVertical: 10,
|
|
92
146
|
},
|
|
93
|
-
|
|
147
|
+
headerInner: {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
alignItems: "center",
|
|
150
|
+
gap: 4,
|
|
151
|
+
},
|
|
152
|
+
bodyRow: {
|
|
94
153
|
flexDirection: "row",
|
|
95
154
|
borderBottomWidth: 1,
|
|
96
155
|
borderBottomColor: colors.border,
|
|
156
|
+
alignItems: "stretch",
|
|
157
|
+
},
|
|
158
|
+
rowPressed: {
|
|
159
|
+
backgroundColor: colors.zinc[50],
|
|
97
160
|
},
|
|
161
|
+
// No fixed height — the row sizes to its content + vertical padding, so rich
|
|
162
|
+
// cells (avatar chips, buttons, stacked badges) get room instead of squeezing.
|
|
98
163
|
bodyCell: {
|
|
99
164
|
minHeight: 44,
|
|
100
165
|
justifyContent: "center",
|
|
101
|
-
paddingHorizontal:
|
|
166
|
+
paddingHorizontal: 12,
|
|
167
|
+
paddingVertical: 12,
|
|
168
|
+
},
|
|
169
|
+
alignEnd: {
|
|
170
|
+
alignItems: "flex-end",
|
|
102
171
|
},
|
|
103
172
|
flexColumn: {
|
|
104
173
|
flex: 1,
|