@lotics/ui 2.4.0 → 2.5.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.
Files changed (69) hide show
  1. package/package.json +27 -8
  2. package/src/accordion.tsx +146 -63
  3. package/src/action_menu.tsx +72 -0
  4. package/src/allocation_row.tsx +54 -0
  5. package/src/avatar.web.tsx +102 -0
  6. package/src/badge.tsx +40 -9
  7. package/src/breakdown.tsx +121 -0
  8. package/src/card.tsx +150 -0
  9. package/src/cell_select.tsx +3 -2
  10. package/src/chip_group.tsx +65 -0
  11. package/src/colors.ts +61 -0
  12. package/src/column_filter.tsx +9 -24
  13. package/src/completion_state.tsx +43 -0
  14. package/src/control_surface.ts +32 -0
  15. package/src/counter.tsx +58 -0
  16. package/src/date_range_filter_field.tsx +44 -12
  17. package/src/detail_row.tsx +45 -0
  18. package/src/dialog.tsx +0 -24
  19. package/src/download.ts +2 -1
  20. package/src/drawer.tsx +94 -2
  21. package/src/empty_state.tsx +37 -0
  22. package/src/file_badge.tsx +27 -4
  23. package/src/file_dropzone.tsx +188 -0
  24. package/src/file_picker.ts +45 -0
  25. package/src/filter_pill.tsx +106 -0
  26. package/src/floating_action_bar.tsx +57 -0
  27. package/src/fonts.css +10 -13
  28. package/src/format_money.ts +38 -0
  29. package/src/heatmap.tsx +153 -0
  30. package/src/icon.tsx +2 -0
  31. package/src/icon_button.tsx +16 -2
  32. package/src/index.css +4 -3
  33. package/src/info_popover.tsx +4 -6
  34. package/src/kpi_card.tsx +19 -6
  35. package/src/kpi_strip.tsx +89 -0
  36. package/src/line_chart.tsx +61 -34
  37. package/src/link_button.tsx +50 -0
  38. package/src/metric.tsx +21 -12
  39. package/src/pagination.tsx +5 -9
  40. package/src/peek.tsx +68 -0
  41. package/src/picker.tsx +13 -1
  42. package/src/picker_menu.tsx +8 -16
  43. package/src/pie_chart.tsx +29 -8
  44. package/src/pill_button.tsx +10 -8
  45. package/src/popover.tsx +14 -4
  46. package/src/pressable_highlight.tsx +10 -1
  47. package/src/pressable_row.tsx +91 -0
  48. package/src/progress_bar.tsx +47 -17
  49. package/src/radio_picker.tsx +20 -9
  50. package/src/range_slider.tsx +185 -0
  51. package/src/remainder_meter.tsx +48 -0
  52. package/src/ring_gauge.tsx +5 -5
  53. package/src/scan_field.tsx +58 -0
  54. package/src/search_input.tsx +12 -0
  55. package/src/sort_header.tsx +102 -0
  56. package/src/stacked_progress_bar.tsx +51 -16
  57. package/src/status_grid.tsx +187 -0
  58. package/src/step_list.tsx +128 -0
  59. package/src/step_progress.tsx +145 -0
  60. package/src/stepper.tsx +9 -4
  61. package/src/table.tsx +168 -112
  62. package/src/text.tsx +15 -0
  63. package/src/text_utils.ts +10 -0
  64. package/src/timeline.tsx +90 -57
  65. package/src/trend_footer.tsx +2 -2
  66. package/src/alert_row.tsx +0 -81
  67. package/src/table.web.tsx +0 -235
  68. package/src/table_picker.tsx +0 -305
  69. package/src/table_types.ts +0 -47
@@ -1,305 +0,0 @@
1
- import { useCallback, useMemo } from "react";
2
- import { StyleSheet, View } from "react-native";
3
- import { colors } from "./colors";
4
- import { Text } from "./text";
5
- import { SearchInput } from "./search_input";
6
- import { ActivityIndicator } from "./activity_indicator";
7
- import { Dialog } from "./dialog";
8
- import { Table } from "@lotics/ui/table";
9
- import type { Column, TableSort } from "@lotics/ui/table_types";
10
- import { Pagination } from "./pagination";
11
- import { ColumnFilter, type FilterableColumn, type ColumnFilterValue } from "./column_filter";
12
-
13
- /** A grid column the picker displays. Sorting is opt-in per column. */
14
- export interface PickerColumn<TRow extends Record<string, unknown>> {
15
- key: string;
16
- label: string;
17
- /** Fixed pixel width. Omit to flex. */
18
- width?: number;
19
- align?: "left" | "right";
20
- /** Show a sortable header for this column. Requires `sort`/`onSortChange`. */
21
- sortable?: boolean;
22
- /** Cell content. Default: the row's value at `key` rendered as text. */
23
- renderCell?: (row: TRow) => React.ReactNode;
24
- }
25
-
26
- /** Single-column sort state — the picker sorts by one column at a time. */
27
- export interface TablePickerSort {
28
- key: string;
29
- order: "asc" | "desc";
30
- }
31
-
32
- export interface TablePickerProps<TRow extends Record<string, unknown>> {
33
- open: boolean;
34
- onOpenChange: (open: boolean) => void;
35
- /** Heading shown above the search box. */
36
- title?: string;
37
-
38
- columns: PickerColumn<TRow>[];
39
- /** Rows of the current page (the consumer paginates). */
40
- rows: TRow[];
41
- /** The field holding each row's stable id — returned by selection and used to
42
- * highlight the picked row (e.g. `"__source_record_id"`). */
43
- rowKey: keyof TRow & string;
44
-
45
- /** Currently selected row id, or null. Highlights the matching row. */
46
- value: string | null;
47
- /** Called with the picked row id. */
48
- onValueChange: (id: string) => void;
49
- /** Close the dialog after a pick. Default `true`. */
50
- closeOnSelect?: boolean;
51
-
52
- /** Controlled search term. The consumer owns it and re-queries. */
53
- searchQuery: string;
54
- onSearchChange: (query: string) => void;
55
- searchPlaceholder?: string;
56
-
57
- /**
58
- * Controlled single-column sort. Omit `onSortChange` to disable sorting.
59
- * Toggling a column cycles asc → desc → off.
60
- */
61
- sort?: TablePickerSort | null;
62
- onSortChange?: (sort: TablePickerSort | null) => void;
63
-
64
- /**
65
- * Per-column filter pills. The consumer holds `filterValues` (keyed by column
66
- * key) and maps them to query conditions via `columnFilterToConditions`.
67
- */
68
- filters?: FilterableColumn[];
69
- filterValues?: Record<string, ColumnFilterValue>;
70
- onFilterChange?: (key: string, value: ColumnFilterValue | undefined) => void;
71
- /** Accessible name for the per-filter clear control. Pass a translated string. */
72
- clearLabel?: string;
73
-
74
- /** Pagination (page-model — the consumer owns the page cursor). */
75
- page: number;
76
- pageSize: number;
77
- /** Total rows across all pages, when known — drives "Page 1 of N". */
78
- total?: number;
79
- hasMore: boolean;
80
- onPageChange: (page: number) => void;
81
-
82
- /** A request is in flight. */
83
- loading?: boolean;
84
- /** Message when there are no rows and nothing is loading. */
85
- emptyLabel?: string;
86
- /** Max dialog width — a browse table needs more room than a form dialog.
87
- * Default 1080. */
88
- maxWidth?: number | `${number}%`;
89
- testID?: string;
90
- }
91
-
92
- /** Render an arbitrary cell value as a single line when no `renderCell` is given. */
93
- function defaultCellText(value: unknown): string {
94
- if (value == null) return "";
95
- if (typeof value === "string") return value;
96
- if (typeof value === "number" || typeof value === "boolean") return String(value);
97
- if (Array.isArray(value)) return value.map(defaultCellText).filter(Boolean).join(", ");
98
- if (typeof value === "object") {
99
- const o = value as { label?: unknown; display?: unknown; name?: unknown };
100
- for (const k of [o.label, o.display, o.name]) {
101
- if (typeof k === "string") return k;
102
- }
103
- }
104
- return "";
105
- }
106
-
107
- /**
108
- * A data-agnostic record-style picker: a modal holding a table the user can
109
- * browse (numbered pages), search, sort, and filter, then pick one row. It knows
110
- * nothing about records/tables/fields — the consumer supplies `columns` + `rows`
111
- * (one page) and owns the search/sort/filter/page state (so the same component
112
- * drives a server query, an in-memory list, or any other source). Compose it
113
- * with a domain hook (e.g. an app's `usePaginatedQuery`) to build a record
114
- * picker, or with any other row source for a different purpose.
115
- *
116
- * Single-select: clicking a row picks its id; the selected row is highlighted;
117
- * `closeOnSelect` (default) dismisses the modal on pick.
118
- */
119
- export function TablePicker<TRow extends Record<string, unknown>>(
120
- props: TablePickerProps<TRow>,
121
- ) {
122
- const {
123
- open,
124
- onOpenChange,
125
- title,
126
- columns,
127
- rows,
128
- rowKey,
129
- value,
130
- onValueChange,
131
- closeOnSelect = true,
132
- searchQuery,
133
- onSearchChange,
134
- searchPlaceholder,
135
- sort = null,
136
- onSortChange,
137
- filters,
138
- filterValues,
139
- onFilterChange,
140
- clearLabel,
141
- page,
142
- pageSize,
143
- total,
144
- hasMore,
145
- onPageChange,
146
- loading = false,
147
- emptyLabel,
148
- maxWidth = 1080,
149
- testID,
150
- } = props;
151
-
152
- const handleSelect = useCallback(
153
- (row: TRow) => {
154
- onValueChange(String(row[rowKey]));
155
- if (closeOnSelect) onOpenChange(false);
156
- },
157
- [onValueChange, rowKey, closeOnSelect, onOpenChange],
158
- );
159
-
160
- // Toggle this column's sort: asc → desc → off. The consumer holds the state.
161
- const toggleSort = useCallback(
162
- (key: string) => {
163
- if (!onSortChange) return;
164
- if (!sort || sort.key !== key) onSortChange({ key, order: "asc" });
165
- else if (sort.order === "asc") onSortChange({ key, order: "desc" });
166
- else onSortChange(null);
167
- },
168
- [sort, onSortChange],
169
- );
170
-
171
- const tableColumns = useMemo<Column<TRow>[]>(
172
- () =>
173
- columns.map((col) => ({
174
- key: col.key,
175
- label: col.label,
176
- width: col.width,
177
- align: col.align,
178
- sortable: col.sortable && !!onSortChange,
179
- renderCell: ({ row }: { row: TRow }) =>
180
- col.renderCell ? (
181
- col.renderCell(row)
182
- ) : (
183
- <Text size="sm" color="zinc-900" numberOfLines={1}>
184
- {defaultCellText(row[col.key])}
185
- </Text>
186
- ),
187
- })),
188
- [columns, onSortChange],
189
- );
190
-
191
- const tableSort = useMemo<TableSort<TRow> | null>(
192
- () => (sort ? { key: sort.key, dir: sort.order } : null),
193
- [sort],
194
- );
195
-
196
- const rowStyle = useCallback(
197
- (row: TRow) =>
198
- value != null && String(row[rowKey]) === value ? { backgroundColor: colors.blue["50"] } : undefined,
199
- [value, rowKey],
200
- );
201
-
202
- return (
203
- <Dialog open={open} onOpenChange={onOpenChange} height="90%" maxWidth={maxWidth} testID={testID}>
204
- <View style={styles.root}>
205
- {title ? (
206
- <Text size="lg" weight="semibold" color="zinc-900" style={styles.title}>
207
- {title}
208
- </Text>
209
- ) : null}
210
-
211
- <SearchInput
212
- testID="table-picker-search"
213
- value={searchQuery}
214
- onChangeText={onSearchChange}
215
- placeholder={searchPlaceholder}
216
- style={styles.search}
217
- />
218
-
219
- {filters && filters.length > 0 ? (
220
- <View style={styles.filterRow}>
221
- {filters.map((f) => (
222
- <ColumnFilter
223
- key={f.key}
224
- column={f}
225
- value={filterValues?.[f.key]}
226
- onChange={(v) => onFilterChange?.(f.key, v)}
227
- clearLabel={clearLabel}
228
- />
229
- ))}
230
- </View>
231
- ) : null}
232
-
233
- <View style={styles.gridArea}>
234
- {rows.length > 0 ? (
235
- <Table<TRow>
236
- columns={tableColumns}
237
- rows={rows}
238
- rowKey={rowKey}
239
- sort={tableSort}
240
- onSortChange={onSortChange ? (key) => toggleSort(key as string) : undefined}
241
- onRowPress={handleSelect}
242
- rowStyle={rowStyle}
243
- />
244
- ) : (
245
- <View style={styles.center}>
246
- {loading ? (
247
- <ActivityIndicator />
248
- ) : (
249
- <Text size="sm" color="zinc-500">
250
- {emptyLabel ?? "No results"}
251
- </Text>
252
- )}
253
- </View>
254
- )}
255
- </View>
256
-
257
- <Pagination
258
- page={page}
259
- pageSize={pageSize}
260
- rowCount={rows.length}
261
- hasMore={hasMore}
262
- total={total}
263
- loading={loading}
264
- onPageChange={onPageChange}
265
- />
266
- </View>
267
- </Dialog>
268
- );
269
- }
270
-
271
- const styles = StyleSheet.create({
272
- root: {
273
- flex: 1,
274
- width: "100%",
275
- gap: 12,
276
- // Match the Dialog's own horizontal inset (the close button sits at 24) so
277
- // the content isn't flush against the dialog edges.
278
- paddingHorizontal: 24,
279
- paddingBottom: 8,
280
- },
281
- title: {},
282
- search: {
283
- width: "100%",
284
- },
285
- filterRow: {
286
- flexDirection: "row",
287
- flexWrap: "wrap",
288
- alignItems: "center",
289
- gap: 8,
290
- },
291
- gridArea: {
292
- flex: 1,
293
- minHeight: 0,
294
- borderWidth: 1,
295
- borderColor: colors.border,
296
- borderRadius: 12,
297
- overflow: "hidden",
298
- },
299
- center: {
300
- flex: 1,
301
- alignItems: "center",
302
- justifyContent: "center",
303
- padding: 24,
304
- },
305
- });
@@ -1,47 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { ViewStyle } from "react-native";
3
-
4
- export type SortDir = "asc" | "desc";
5
-
6
- export interface TableSort<TRow extends Record<string, unknown>> {
7
- key: keyof TRow;
8
- dir: SortDir;
9
- }
10
-
11
- export interface Column<TRow extends Record<string, unknown>> {
12
- key: keyof TRow;
13
- label: string;
14
- width?: number;
15
- /** Horizontal alignment of header + cell content. Default "left". */
16
- align?: "left" | "right";
17
- /** When true (and `onSortChange` is set), the header is pressable + shows a
18
- * sort arrow when this column is the active `sort`. */
19
- sortable?: boolean;
20
- renderCell?: (params: { row: TRow; column: Column<TRow> }) => ReactNode;
21
- }
22
-
23
- export interface TableProps<TRow extends Record<string, unknown>> {
24
- columns: Column<TRow>[];
25
- rows: (TRow | null | false)[];
26
- /** Stable per-row key. Required when `renderDetail` is set (expansion is
27
- * tracked by this key, not row index, so it survives sort/filter). */
28
- rowKey?: keyof TRow;
29
- rowStyle?: (row: TRow) => ViewStyle | undefined;
30
- /** Current sort. Pair with `onSortChange` to render sortable headers; the
31
- * parent owns the actual sorting of `rows` — this only drives the indicator. */
32
- sort?: TableSort<TRow> | null;
33
- onSortChange?: (key: keyof TRow) => void;
34
- /** Click-to-select: pressing a row (except its interactive controls) calls
35
- * this — e.g. a picker that returns the chosen row. Mutually exclusive with
36
- * `renderDetail` (a row either expands or selects); `renderDetail` wins if
37
- * both are set. Pair with `rowStyle` to highlight the selected row. */
38
- onRowPress?: (row: TRow) => void;
39
- /** Render an inline detail panel, full-width below the row. When set, the
40
- * whole row (except its interactive controls) is click-to-expand. The row
41
- * detects controls by element/role, so cells need no special marking. */
42
- renderDetail?: (row: TRow) => ReactNode;
43
- /** Controlled set of expanded row keys. Omit for internal (uncontrolled) state. */
44
- expandedKeys?: Set<string>;
45
- /** Called when a row's expand state toggles (with its `rowKey` value). */
46
- onToggleRow?: (key: string, row: TRow) => void;
47
- }