@lotics/ui 21.0.0 → 21.1.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/docs/catalog.md +7 -2
- package/package.json +1 -1
- package/src/data_grid.tsx +44 -11
- package/src/ledger.tsx +16 -3
package/docs/catalog.md
CHANGED
|
@@ -137,8 +137,13 @@ Two columnar shapes, and the choice is about data size:
|
|
|
137
137
|
sortable header + collapsible grouped sections + aligned rows + an optional per-row
|
|
138
138
|
`leading` slot (a `CheckCircle`); YOU own the data, the sort/group/filter/collapse STATE
|
|
139
139
|
(`cycleSort` + `sortBy` from `@lotics/ui/sort_header`, a `collapsed` Set), the toolbar
|
|
140
|
-
(`SearchInput` + `FilterChip`s)
|
|
141
|
-
|
|
140
|
+
(`SearchInput` + `FilterChip`s). A column's `align: "right"` moves BOTH its cells and its
|
|
141
|
+
heading, so digits line up under their label; **`DataGridColumn.footer` gives that column its
|
|
142
|
+
cell in the group's footer row** (a subtotal, a resolved measure) — the grid lays it out in
|
|
143
|
+
the column's own box, so a total can never drift from what it sums and no consumer rebuilds
|
|
144
|
+
the row. `renderGroupFooter` remains for content that does NOT sit in a column (a per-group
|
|
145
|
+
add row). The grid takes exactly its container's width — no outdent to fight, so it needs no
|
|
146
|
+
scroller unless YOU add one. It renders ALL rows (no
|
|
142
147
|
virtualization), so it's only for sets small enough to hold in view — at 10k+ it lags, and
|
|
143
148
|
grouping + pagination/infinite don't compose; use the register instead. Worked example:
|
|
144
149
|
[`tpl_task_board`](../examples/tpl_task_board.tsx) (a `CheckCircle` leading).
|
package/package.json
CHANGED
package/src/data_grid.tsx
CHANGED
|
@@ -11,9 +11,17 @@ export interface DataGridColumn<T> {
|
|
|
11
11
|
label: string;
|
|
12
12
|
/** Fixed width in px; omit for the flexible primary column (usually the first). */
|
|
13
13
|
width?: number;
|
|
14
|
+
/** Cell + header alignment. Money and counts read RIGHT so digits line up under
|
|
15
|
+
* their heading; text reads left (the default). */
|
|
16
|
+
align?: "left" | "right";
|
|
14
17
|
sortable?: boolean;
|
|
15
18
|
/** Render the cell for this column — any field: a `Text`, an inline editor, a badge. */
|
|
16
19
|
cell: (item: T) => ReactNode;
|
|
20
|
+
/** This column's cell in the group's FOOTER row — a subtotal, a resolved measure.
|
|
21
|
+
* Declared per COLUMN so the grid lays it out in that column's own box: a total
|
|
22
|
+
* can never drift from the values it sums, and no consumer re-implements the row.
|
|
23
|
+
* The footer row renders when ANY column defines one. */
|
|
24
|
+
footer?: (group: DataGridGroup<T>) => ReactNode;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface DataGridGroup<T> {
|
|
@@ -39,7 +47,9 @@ export interface DataGridProps<T> {
|
|
|
39
47
|
/** Controlled collapse — the set of collapsed group keys. */
|
|
40
48
|
collapsed?: Set<string>;
|
|
41
49
|
onToggleCollapse?: (key: string) => void;
|
|
42
|
-
/** Rendered under each group's rows
|
|
50
|
+
/** Rendered under each group's rows AND under the columns' own footer row —
|
|
51
|
+
* for content that does NOT sit in a column, typically a per-group add row.
|
|
52
|
+
* For a column-aligned subtotal use `DataGridColumn.footer`. */
|
|
43
53
|
renderGroupFooter?: (group: DataGridGroup<T>) => ReactNode;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -57,13 +67,19 @@ export interface DataGridProps<T> {
|
|
|
57
67
|
export function DataGrid<T>(props: DataGridProps<T>) {
|
|
58
68
|
const { columns, groups, getRowKey, leading, sort, onSort, labels, collapsed, onToggleCollapse, renderGroupFooter } = props;
|
|
59
69
|
const lead = leading?.width ?? 0;
|
|
70
|
+
const hasFooter = columns.some((c) => c.footer != null);
|
|
60
71
|
|
|
61
72
|
return (
|
|
62
73
|
<View>
|
|
63
74
|
<View style={[styles.row, styles.head]}>
|
|
64
75
|
{lead > 0 ? <View style={{ width: lead }} /> : null}
|
|
76
|
+
{/* The header label rides the SAME column box as the cells below it, so a
|
|
77
|
+
right-aligned column's heading sits over its digits instead of drifting
|
|
78
|
+
to the column's left edge. */}
|
|
65
79
|
{columns.map((c) => (
|
|
66
|
-
<
|
|
80
|
+
<View key={c.key} style={colStyle(c)}>
|
|
81
|
+
<SortLabel label={c.label} sortKey={c.key} sortable={c.sortable !== false && !!onSort} sort={sort ?? null} onSort={onSort} labels={labels} />
|
|
82
|
+
</View>
|
|
67
83
|
))}
|
|
68
84
|
</View>
|
|
69
85
|
|
|
@@ -95,6 +111,14 @@ export function DataGrid<T>(props: DataGridProps<T>) {
|
|
|
95
111
|
))}
|
|
96
112
|
</View>
|
|
97
113
|
))}
|
|
114
|
+
{hasFooter ? (
|
|
115
|
+
<View style={styles.row}>
|
|
116
|
+
{lead > 0 ? <View style={{ width: lead }} /> : null}
|
|
117
|
+
{columns.map((c) => (
|
|
118
|
+
<View key={c.key} style={colStyle(c)}>{c.footer?.(g)}</View>
|
|
119
|
+
))}
|
|
120
|
+
</View>
|
|
121
|
+
) : null}
|
|
98
122
|
{renderGroupFooter?.(g)}
|
|
99
123
|
</>
|
|
100
124
|
) : null}
|
|
@@ -105,13 +129,20 @@ export function DataGrid<T>(props: DataGridProps<T>) {
|
|
|
105
129
|
);
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
/** The shared column box — width (or flex) plus alignment. ONE definition, used by
|
|
133
|
+
* the header, every cell and every footer cell, so the three cannot drift. */
|
|
134
|
+
function colStyleImpl(c: { width?: number; align?: "left" | "right" }): StyleProp<ViewStyle> {
|
|
135
|
+
return [
|
|
136
|
+
c.width != null ? { width: c.width } : styles.flexCol,
|
|
137
|
+
c.align === "right" ? { alignItems: "flex-end" as const } : null,
|
|
138
|
+
];
|
|
110
139
|
}
|
|
140
|
+
const colStyle = colStyleImpl;
|
|
111
141
|
|
|
112
|
-
// The
|
|
113
|
-
//
|
|
114
|
-
//
|
|
142
|
+
// The column heading. It sits in the SAME column box as the cells, and its own
|
|
143
|
+
// padding is cancelled by an equal negative margin — so the hover pill has room
|
|
144
|
+
// while the label still starts exactly on its cell's edge. Non-sortable headings
|
|
145
|
+
// render as plain text: no press, no pointer, no button in the a11y tree.
|
|
115
146
|
function SortLabel(props: { label: string; sortKey: string; sortable: boolean; sort: SortState | null; onSort?: (k: string) => void; labels?: SortHeaderLabels; style?: StyleProp<ViewStyle> }) {
|
|
116
147
|
const { label, sortKey, sortable, sort, onSort, labels, style } = props;
|
|
117
148
|
const active = sort?.key === sortKey;
|
|
@@ -126,7 +157,7 @@ function SortLabel(props: { label: string; sortKey: string; sortable: boolean; s
|
|
|
126
157
|
const sortByLabel = (labels?.sortBy ?? ((l: string) => `Sort by ${l}`))(label);
|
|
127
158
|
const dirText = active ? (sort?.dir === "asc" ? (labels?.ascending ?? ", ascending") : (labels?.descending ?? ", descending")) : "";
|
|
128
159
|
return (
|
|
129
|
-
<FocusRingPressable onPress={() => onSort?.(sortKey)} accessibilityRole="button" accessibilityLabel={`${sortByLabel}${dirText}`} style={({ hovered }: { hovered?: boolean }) => [styles.sortLabel, hovered ? styles.sortLabelHover : null, style]}>
|
|
160
|
+
<FocusRingPressable onPress={() => onSort?.(sortKey)} accessibilityRole="button" accessibilityLabel={`${sortByLabel}${dirText}`} style={({ hovered }: { hovered?: boolean }) => [styles.sortLabel, styles.sortLabelPressable, hovered ? styles.sortLabelHover : null, style]}>
|
|
130
161
|
<Text size="xs" weight="semibold" color={active ? "default" : "muted"} numberOfLines={1} style={styles.headLabel}>{label}</Text>
|
|
131
162
|
{arrow ? <Icon name={arrow} size={12} color={colors.zinc[500]} /> : null}
|
|
132
163
|
</FocusRingPressable>
|
|
@@ -139,7 +170,6 @@ export const gridRowStyle: ViewStyle = {
|
|
|
139
170
|
alignItems: "center",
|
|
140
171
|
gap: 12,
|
|
141
172
|
minHeight: 44,
|
|
142
|
-
paddingHorizontal: 8,
|
|
143
173
|
};
|
|
144
174
|
|
|
145
175
|
const styles = StyleSheet.create({
|
|
@@ -152,9 +182,12 @@ const styles = StyleSheet.create({
|
|
|
152
182
|
marginBottom: 4,
|
|
153
183
|
},
|
|
154
184
|
headLabel: { letterSpacing: 0.3, textTransform: "uppercase" },
|
|
155
|
-
|
|
185
|
+
// Padded for the hover pill but zero-width in flow (negative margin), so the label
|
|
186
|
+
// sits on its cell's edge instead of 8px inside it.
|
|
187
|
+
sortLabel: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 6, marginHorizontal: -6, paddingVertical: 2, borderRadius: 6 },
|
|
188
|
+
sortLabelPressable: { cursor: "pointer" },
|
|
156
189
|
sortLabelHover: { backgroundColor: colors.zinc[50] },
|
|
157
190
|
groupSep: { borderTopWidth: 1, borderTopColor: colors.zinc[100], marginTop: 8, paddingTop: 8 },
|
|
158
|
-
section: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, paddingVertical: 7, borderRadius: 8 },
|
|
191
|
+
section: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 8, marginHorizontal: -8, paddingVertical: 7, borderRadius: 8 },
|
|
159
192
|
sectionHover: { backgroundColor: colors.zinc[50] },
|
|
160
193
|
});
|
package/src/ledger.tsx
CHANGED
|
@@ -77,7 +77,7 @@ export function LedgerGroup(props: LedgerGroupProps) {
|
|
|
77
77
|
const { format } = useLedger();
|
|
78
78
|
return (
|
|
79
79
|
<View style={styles.group}>
|
|
80
|
-
<View style={styles.
|
|
80
|
+
<View style={styles.groupHead}>
|
|
81
81
|
<Text size="xs" weight="medium" color="muted" style={styles.grow}>
|
|
82
82
|
{label}
|
|
83
83
|
</Text>
|
|
@@ -198,8 +198,11 @@ const styles = StyleSheet.create({
|
|
|
198
198
|
// Outdent by the rows' 8px inset (the ListItem/Timeline technique) so row text +
|
|
199
199
|
// the money column align with the section heading and the container edges;
|
|
200
200
|
// the pressable door's wash bleeds into the gutter instead of squeezing text.
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// Grouping reads by PROXIMITY: the gap between groups must be decisively larger
|
|
202
|
+
// than the gap inside one, or the eyebrow floats midway and belongs to neither
|
|
203
|
+
// the group above nor the rows below. 18 between, 2 within.
|
|
204
|
+
ledger: { gap: 16, marginHorizontal: -8 },
|
|
205
|
+
group: { gap: 0 },
|
|
203
206
|
// EVERY line (group header, rows, the total) shares the 8px text inset, so
|
|
204
207
|
// labels and the money column sit on one edge whether a row peeks or not —
|
|
205
208
|
// the pressable door's wash simply fills the same padded box.
|
|
@@ -210,6 +213,16 @@ const styles = StyleSheet.create({
|
|
|
210
213
|
minHeight: 28,
|
|
211
214
|
paddingHorizontal: 8,
|
|
212
215
|
},
|
|
216
|
+
// The group eyebrow is a CAPTION on the rows beneath it, not a line of its own —
|
|
217
|
+
// it takes only the height its xs text needs, so it hugs what it labels instead
|
|
218
|
+
// of sitting in a full 28px row box.
|
|
219
|
+
groupHead: {
|
|
220
|
+
flexDirection: "row",
|
|
221
|
+
alignItems: "center",
|
|
222
|
+
gap: 10,
|
|
223
|
+
minHeight: 16,
|
|
224
|
+
paddingHorizontal: 8,
|
|
225
|
+
},
|
|
213
226
|
rowPress: {
|
|
214
227
|
borderRadius: 8,
|
|
215
228
|
},
|