@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/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.headerCell,
99
+ styles.bodyCell,
100
+ column.align === "right" ? styles.alignEnd : null,
38
101
  ]}
39
- key={column.key as string}
40
102
  >
41
- <Text weight="medium" numberOfLines={1} userSelect="none">
42
- {column.label}
43
- </Text>
103
+ {column.renderCell ? (
104
+ column.renderCell({ row, column })
105
+ ) : (
106
+ <Text numberOfLines={1}>{row[column.key]?.toString()}</Text>
107
+ )}
44
108
  </View>
45
- ))}
46
- </View>
47
- {rows.filter(Boolean).map((r, i) => {
48
- const row = r as TRow;
49
- const extraStyle = rowStyle?.(row);
50
- return (
51
- <View
52
- key={rowKey ? String(row[rowKey]) : i}
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
- {columns.map((column) => (
56
- <View
57
- key={column.key as string}
58
- style={[
59
- column.width ? { width: column.width } : styles.flexColumn,
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
- height: 44,
142
+ minHeight: 40,
90
143
  justifyContent: "center",
91
- paddingHorizontal: 8,
144
+ paddingHorizontal: 12,
145
+ paddingVertical: 10,
92
146
  },
93
- bodyRow: {
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: 8,
166
+ paddingHorizontal: 12,
167
+ paddingVertical: 12,
168
+ },
169
+ alignEnd: {
170
+ alignItems: "flex-end",
102
171
  },
103
172
  flexColumn: {
104
173
  flex: 1,