@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
package/src/timeline.tsx CHANGED
@@ -2,7 +2,7 @@ import { ReactNode, useCallback, useState } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
  import { ActivityIndicator } from "./activity_indicator";
4
4
  import { AnimationFadeIn } from "./animation_fade_in";
5
- import { colors } from "./colors";
5
+ import { colors, withAlpha } from "./colors";
6
6
  import { Icon, type IconName } from "./icon";
7
7
  import { PressableHighlight } from "./pressable_highlight";
8
8
  import { Text } from "./text";
@@ -10,9 +10,14 @@ import { Text } from "./text";
10
10
  export interface TimelineItem {
11
11
  id: string;
12
12
  icon: IconName;
13
+ /** Accent for the node — a palette hex (e.g. colors.blue[600]). The node
14
+ * renders a tinted disc of this color; the icon takes the full color. */
13
15
  iconColor: string;
14
16
  isLoading?: boolean;
15
17
  label: string;
18
+ /** An always-visible sub-line under the label (a note, a detail). When set, the
19
+ * row top-aligns so `right` sits next to the label, not centred on the block. */
20
+ description?: string;
16
21
  error?: string;
17
22
  right?: ReactNode;
18
23
  details?: ReactNode;
@@ -22,6 +27,12 @@ interface TimelineProps {
22
27
  items: TimelineItem[];
23
28
  }
24
29
 
30
+ /**
31
+ * Vertical event log — tinted icon nodes on a hairline spine, one row per
32
+ * event. Rows are plain (no boxes — the spine provides the structure);
33
+ * a row becomes pressable only when it carries `details`, revealing them
34
+ * inline. For horizontal milestone progress use `Stepper`.
35
+ */
25
36
  export function Timeline(props: TimelineProps) {
26
37
  const { items } = props;
27
38
  const [expandedIds, setExpandedIds] = useState<Record<string, boolean>>({});
@@ -31,52 +42,56 @@ export function Timeline(props: TimelineProps) {
31
42
  }, []);
32
43
 
33
44
  return (
34
- <View style={styles.container}>
45
+ <View>
35
46
  {items.map((item, index) => {
36
47
  const expanded = expandedIds[item.id] ?? false;
37
48
  const hasDetails = !!item.details;
38
49
 
50
+ const row = (
51
+ <View style={[styles.row, item.description ? styles.rowTop : null]}>
52
+ <View style={{ flex: 1, gap: 2 }}>
53
+ <Text size="sm">{item.label}</Text>
54
+ {item.description ? (
55
+ <Text size="xs" color="muted">{item.description}</Text>
56
+ ) : null}
57
+ {item.error ? (
58
+ <Text size="xs" color="danger" numberOfLines={1}>
59
+ {item.error}
60
+ </Text>
61
+ ) : null}
62
+ </View>
63
+ {item.right}
64
+ {hasDetails ? (
65
+ <Icon name={expanded ? "chevron-up" : "chevron-down"} size={14} color={colors.zinc[400]} />
66
+ ) : null}
67
+ </View>
68
+ );
69
+
39
70
  return (
40
71
  <View key={item.id} style={styles.itemContainer}>
41
- <View style={styles.timelineColumn}>
72
+ <View style={styles.spineColumn}>
42
73
  <AnimationFadeIn key={`${item.id}-${item.icon}`}>
43
- <View style={[styles.iconContainer, { borderColor: item.iconColor }]}>
74
+ {/* Tinted disc: a low-alpha wash of the accent behind a full-color icon. */}
75
+ <View style={[styles.node, { backgroundColor: withAlpha(item.iconColor, 0.1) }]}>
44
76
  {item.isLoading ? (
45
- <ActivityIndicator size={12} color={item.iconColor} />
77
+ <ActivityIndicator size={13} color={item.iconColor} />
46
78
  ) : (
47
- <Icon name={item.icon} size={12} color={item.iconColor} />
79
+ <Icon name={item.icon} size={13} color={item.iconColor} />
48
80
  )}
49
81
  </View>
50
82
  </AnimationFadeIn>
51
- {index < items.length - 1 && <View style={styles.line} />}
83
+ {index < items.length - 1 && <View style={styles.spine} />}
52
84
  </View>
53
85
 
54
86
  <View style={styles.contentColumn}>
55
- <PressableHighlight
56
- onPress={hasDetails ? () => toggleItem(item.id) : undefined}
57
- style={styles.itemHeader}
58
- >
59
- <View style={{ flex: 1 }}>
60
- <Text size="sm">{item.label}</Text>
61
- {item.error && (
62
- <Text size="xs" color="danger" numberOfLines={1}>
63
- {item.error}
64
- </Text>
65
- )}
66
- </View>
67
- {item.right}
68
- {hasDetails && (
69
- <Icon
70
- name={expanded ? "chevron-up" : "chevron-down"}
71
- size={14}
72
- color={colors.zinc[500]}
73
- />
74
- )}
75
- </PressableHighlight>
76
-
77
- {expanded && item.details && (
78
- <View style={styles.detailsContainer}>{item.details}</View>
87
+ {hasDetails ? (
88
+ <PressableHighlight onPress={() => toggleItem(item.id)} style={styles.pressableRow}>
89
+ {row}
90
+ </PressableHighlight>
91
+ ) : (
92
+ <View style={styles.plainRow}>{row}</View>
79
93
  )}
94
+ {expanded && item.details ? <View style={styles.detailsContainer}>{item.details}</View> : null}
80
95
  </View>
81
96
  </View>
82
97
  );
@@ -86,50 +101,68 @@ export function Timeline(props: TimelineProps) {
86
101
  }
87
102
 
88
103
  const styles = StyleSheet.create({
89
- container: {
90
- gap: 0,
91
- },
92
104
  itemContainer: {
93
105
  flexDirection: "row",
94
- gap: 8,
106
+ gap: 12,
95
107
  },
96
- timelineColumn: {
97
- width: 16,
108
+ spineColumn: {
109
+ width: 24,
98
110
  alignItems: "center",
111
+ // Align the node with the FIRST line of content (not the centre of a
112
+ // multi-line block), so the spine reads as connected.
113
+ paddingTop: 2,
99
114
  },
100
- iconContainer: {
101
- width: 16,
102
- height: 16,
103
- borderRadius: 8,
104
- borderWidth: 1,
115
+ node: {
116
+ width: 24,
117
+ height: 24,
118
+ borderRadius: 12,
105
119
  justifyContent: "center",
106
120
  alignItems: "center",
107
- backgroundColor: colors.white,
108
121
  },
109
- line: {
110
- width: 1,
122
+ spine: {
123
+ width: 1.5,
111
124
  flex: 1,
112
- minHeight: 24,
113
- backgroundColor: colors.zinc[300],
114
- marginTop: 4,
125
+ minHeight: 12,
126
+ borderRadius: 1,
127
+ backgroundColor: colors.zinc[200],
128
+ marginVertical: 3,
115
129
  },
116
130
  contentColumn: {
117
131
  flex: 1,
118
- gap: 8,
132
+ paddingBottom: 14,
119
133
  },
120
- itemHeader: {
134
+ row: {
121
135
  flexDirection: "row",
122
136
  alignItems: "center",
123
- gap: 8,
124
- borderWidth: 1,
125
- borderColor: colors.zinc[200],
137
+ gap: 10,
138
+ flex: 1,
139
+ },
140
+ // With a description the row is taller than one line — top-align so the
141
+ // timestamp (`right`) sits beside the label instead of centred on the block.
142
+ rowTop: {
143
+ alignItems: "flex-start",
144
+ },
145
+ // The press target for expandable rows; pressable rows bleed the hover wash
146
+ // past the text column. paddingVertical pairs with the spineColumn paddingTop
147
+ // so the node lines up with the first line.
148
+ pressableRow: {
126
149
  borderRadius: 8,
127
- backgroundColor: colors.white,
128
- paddingHorizontal: 10,
129
- paddingVertical: 8,
150
+ paddingHorizontal: 8,
151
+ paddingVertical: 4,
152
+ marginHorizontal: -8,
153
+ minHeight: 28,
154
+ flexDirection: "row",
155
+ alignItems: "center",
156
+ },
157
+ plainRow: {
158
+ paddingVertical: 4,
159
+ minHeight: 28,
160
+ flexDirection: "row",
161
+ alignItems: "center",
130
162
  },
131
163
  detailsContainer: {
132
164
  gap: 8,
133
- paddingLeft: 4,
165
+ paddingTop: 8,
166
+ paddingLeft: 2,
134
167
  },
135
168
  });
@@ -20,7 +20,7 @@ interface TrendFooterProps {
20
20
 
21
21
  /**
22
22
  * Footer caption that pairs the shadcn TrendingUp icon with a directional
23
- * Vietnamese sentence — the "Tăng X% so với tháng trước" pattern at the
23
+ * sentence — the "Up X% vs last month" pattern at the
24
24
  * bottom of every chart card on Stripe, Mercury, Linear.
25
25
  *
26
26
  * Goes inside `<SectionCard footer={...} />`. The Card adds the hairline
@@ -38,7 +38,7 @@ export function TrendFooter(props: TrendFooterProps) {
38
38
  <View style={styles.row}>
39
39
  <Icon size={16} color={color} />
40
40
  <Text size="sm" weight="medium" style={{ color }}>
41
- {up ? "Tăng" : "Giảm"} {Math.abs(value)}% {periodLabel}
41
+ {up ? "Up" : "Down"} {Math.abs(value)}% {periodLabel}
42
42
  </Text>
43
43
  </View>
44
44
  {detail && (
package/src/alert_row.tsx DELETED
@@ -1,81 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { Text } from "./text";
4
- import { Metric, type MetricSize, type MetricTone } from "./metric";
5
- import { colors } from "./colors";
6
- import { SPACE } from "./spacing";
7
-
8
- interface AlertRowProps {
9
- /** Leading icon (lucide-react `<AlertCircle />`, `<CheckCircle2 />`, etc.).
10
- * Sized 18px in the host. Caller controls color so different severities
11
- * can use different palettes. */
12
- icon: ReactNode;
13
- label: string;
14
- /** The count or quantity. `null` while loading → renders as `emptyLabel`. */
15
- count: number | string | null;
16
- /** Small text below the count — unit ("Hồ sơ"), amount caption
17
- * ("12.000.000 đ"), or short hint. */
18
- hint?: string;
19
- /** Number size. Default `lg` keeps the row scannable; `md` for denser
20
- * lists where many rows compete. */
21
- size?: MetricSize;
22
- /** Auto-set: non-zero count = danger (red), zero = default. Override for
23
- * status semantics (e.g., when down is good). */
24
- tone?: MetricTone;
25
- /** Suppress the bottom hairline. Set on the last row of a list. */
26
- last?: boolean;
27
- }
28
-
29
- /**
30
- * One row of a prioritized action list — icon + label on the left, count
31
- * + hint on the right. The Mercury / Linear "needs attention" pattern.
32
- *
33
- * Layout enforces two-column rhythm: left side flex-grows with the label,
34
- * right side is fixed-width and right-aligned. Without enforcement, rows
35
- * drift into "label centered, count floating left of right edge" which
36
- * makes the list unscannable.
37
- */
38
- export function AlertRow(props: AlertRowProps) {
39
- const { icon, label, count, hint, size = "lg", tone, last } = props;
40
- const hasIssue = typeof count === "number" && count > 0;
41
- const resolvedTone: MetricTone = tone ?? (hasIssue ? "danger" : "default");
42
- return (
43
- <View style={[styles.row, !last && styles.divider]}>
44
- <View style={styles.leftCol}>
45
- {icon}
46
- <Text size="sm">{label}</Text>
47
- </View>
48
- <View style={styles.rightCol}>
49
- <Metric value={count} size={size} tone={resolvedTone} />
50
- {hint && (
51
- <Text size="xs" color="muted">
52
- {hint}
53
- </Text>
54
- )}
55
- </View>
56
- </View>
57
- );
58
- }
59
-
60
- const styles = StyleSheet.create({
61
- row: {
62
- flexDirection: "row",
63
- alignItems: "center",
64
- justifyContent: "space-between",
65
- paddingVertical: SPACE.md,
66
- },
67
- divider: {
68
- borderBottomWidth: 1,
69
- borderBottomColor: colors.zinc[100],
70
- },
71
- leftCol: {
72
- flexDirection: "row",
73
- alignItems: "center",
74
- gap: SPACE.md,
75
- flex: 1,
76
- },
77
- rightCol: {
78
- alignItems: "flex-end",
79
- minWidth: 100,
80
- },
81
- });
package/src/table.web.tsx DELETED
@@ -1,235 +0,0 @@
1
- import { CSSProperties, Fragment, useState } from "react";
2
- import { Text } from "./text";
3
- import { Icon } from "./icon";
4
- import { colors } from "./colors";
5
- import type { Column, TableProps } from "./table_types";
6
-
7
- export type { SortDir, TableSort, Column, TableProps } from "./table_types";
8
-
9
- // Web table. Built from raw <div>s (not RN Pressable) so a click-to-expand row
10
- // can legally contain interactive controls: the row is a <div role="row">, never
11
- // a <button>. A row click toggles expansion EXCEPT when it lands on an actual
12
- // interactive element — those keep their own behaviour. This is the web mirror of
13
- // RN's responder model (the innermost control wins): only the control suppresses
14
- // the toggle, so empty space anywhere in the row — including around a control in a
15
- // wide action column — still expands. No stopPropagation, no nested <button>, no
16
- // absolute overlay, no dead zones. The detail panel renders full-width below.
17
-
18
- const CHEVRON_W = 44;
19
-
20
- // Clicks landing on (or inside) one of these keep their own behaviour instead of
21
- // toggling the row — the standard interactive HTML tags + ARIA interactive roles,
22
- // plus an explicit [data-interactive] escape hatch for a non-element control.
23
- const INTERACTIVE_SELECTOR =
24
- 'a[href], button, input, select, textarea, label, summary, [role="button"], [role="link"], [role="checkbox"], [role="switch"], [role="radio"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="tab"], [role="slider"], [role="spinbutton"], [contenteditable="true"], [data-interactive]';
25
-
26
- function colWidth<TRow extends Record<string, unknown>>(col: Column<TRow>): CSSProperties {
27
- return col.width ? { width: col.width, flexShrink: 0 } : { flex: 1, minWidth: 0 };
28
- }
29
-
30
- export function Table<TRow extends Record<string, unknown>>(props: TableProps<TRow>) {
31
- const { columns, rows, rowKey, rowStyle, sort, onSortChange, renderDetail, expandedKeys, onToggleRow, onRowPress } = props;
32
- const [internal, setInternal] = useState<Set<string>>(() => new Set());
33
- const [hoverKey, setHoverKey] = useState<string | null>(null);
34
- const expanded = expandedKeys ?? internal;
35
- const expandable = !!renderDetail;
36
- // A row reacts to clicks for one of two reasons: expansion (renderDetail) or
37
- // selection (onRowPress). Expansion wins if both are set.
38
- const pressable = expandable || !!onRowPress;
39
-
40
- const toggle = (key: string, row: TRow) => {
41
- onToggleRow?.(key, row);
42
- if (expandedKeys === undefined) {
43
- setInternal((prev) => {
44
- const next = new Set(prev);
45
- if (next.has(key)) next.delete(key);
46
- else next.add(key);
47
- return next;
48
- });
49
- }
50
- };
51
-
52
- const visibleRows = rows.filter((r): r is TRow => Boolean(r));
53
-
54
- return (
55
- <div style={containerStyle} role="table">
56
- {/* Header */}
57
- <div style={headerRowStyle} role="row">
58
- {columns.map((col) => {
59
- const sortable = col.sortable && !!onSortChange;
60
- const active = sort?.key === col.key;
61
- const style: CSSProperties = {
62
- ...headerCellStyle,
63
- ...colWidth(col),
64
- justifyContent: col.align === "right" ? "flex-end" : "flex-start",
65
- cursor: sortable ? "pointer" : "default",
66
- };
67
- const inner = (
68
- <>
69
- <Text size="xs" weight="medium" color="muted" numberOfLines={1} userSelect="none" transform="uppercase">
70
- {col.label}
71
- </Text>
72
- {sortable ? (
73
- <Icon
74
- name={active ? (sort?.dir === "asc" ? "chevron-up" : "chevron-down") : "chevrons-up-down"}
75
- size={14}
76
- color={active ? colors.zinc[700] : colors.zinc[400]}
77
- />
78
- ) : null}
79
- </>
80
- );
81
- return sortable ? (
82
- <div
83
- key={col.key as string}
84
- role="columnheader"
85
- aria-sort={active ? (sort?.dir === "asc" ? "ascending" : "descending") : undefined}
86
- tabIndex={0}
87
- onClick={() => onSortChange?.(col.key)}
88
- onKeyDown={(e) => {
89
- if (e.key === "Enter" || e.key === " ") {
90
- e.preventDefault();
91
- onSortChange?.(col.key);
92
- }
93
- }}
94
- style={style}
95
- >
96
- {inner}
97
- </div>
98
- ) : (
99
- <div key={col.key as string} role="columnheader" style={style}>
100
- {inner}
101
- </div>
102
- );
103
- })}
104
- {expandable ? <div style={{ width: CHEVRON_W, flexShrink: 0 }} /> : null}
105
- </div>
106
-
107
- {/* Body */}
108
- {visibleRows.map((row, i) => {
109
- const key = rowKey ? String(row[rowKey]) : String(i);
110
- const isOpen = expanded.has(key);
111
- const extra = (rowStyle?.(row) as CSSProperties | undefined) ?? undefined;
112
- const rowStyleFinal: CSSProperties = {
113
- ...bodyRowStyle,
114
- cursor: pressable ? "pointer" : "default",
115
- background: hoverKey === key && pressable ? colors.zinc[50] : colors.white,
116
- ...extra,
117
- };
118
- return (
119
- <Fragment key={key}>
120
- <div
121
- role="row"
122
- onClick={
123
- pressable
124
- ? (e: React.MouseEvent) => {
125
- // Whole-row click acts (expand or select), EXCEPT when it lands on an
126
- // actual interactive element — those keep their own behaviour. The
127
- // disclosure chevron is the keyboard/AT affordance for expansion.
128
- if ((e.target as HTMLElement).closest(INTERACTIVE_SELECTOR)) return;
129
- if (expandable) toggle(key, row);
130
- else onRowPress?.(row);
131
- }
132
- : undefined
133
- }
134
- onMouseEnter={pressable ? () => setHoverKey(key) : undefined}
135
- onMouseLeave={pressable ? () => setHoverKey((k) => (k === key ? null : k)) : undefined}
136
- style={rowStyleFinal}
137
- >
138
- {columns.map((col) => (
139
- <div
140
- key={col.key as string}
141
- role="cell"
142
- style={{
143
- ...bodyCellStyle,
144
- ...colWidth(col),
145
- alignItems: col.align === "right" ? "flex-end" : "flex-start",
146
- }}
147
- >
148
- {col.renderCell ? (
149
- col.renderCell({ row, column: col })
150
- ) : (
151
- <Text numberOfLines={1}>{row[col.key] != null ? String(row[col.key]) : ""}</Text>
152
- )}
153
- </div>
154
- ))}
155
- {expandable ? (
156
- <button
157
- type="button"
158
- data-interactive
159
- aria-expanded={isOpen}
160
- aria-label={isOpen ? "Collapse row" : "Expand row"}
161
- onClick={() => toggle(key, row)}
162
- style={chevronButtonStyle}
163
- >
164
- <Icon name={isOpen ? "chevron-up" : "chevron-down"} size={18} color={colors.zinc[400]} />
165
- </button>
166
- ) : null}
167
- </div>
168
- {expandable && isOpen ? (
169
- <div role="row" style={detailRowStyle}>
170
- <div role="cell" style={detailCellStyle}>
171
- {renderDetail!(row)}
172
- </div>
173
- </div>
174
- ) : null}
175
- </Fragment>
176
- );
177
- })}
178
- </div>
179
- );
180
- }
181
-
182
- // `maxHeight: 100%` is a no-op in a content-sized parent (a card grows to its
183
- // rows) but caps the table to a bounded parent (a modal's flex region), so
184
- // `overflow: auto` then scrolls and the sticky header engages.
185
- const containerStyle: CSSProperties = { width: "100%", maxHeight: "100%", overflow: "auto" };
186
- const headerRowStyle: CSSProperties = {
187
- display: "flex",
188
- position: "sticky",
189
- top: 0,
190
- zIndex: 1,
191
- background: colors.white,
192
- borderBottom: `1px solid ${colors.border}`,
193
- };
194
- const headerCellStyle: CSSProperties = {
195
- minHeight: 40,
196
- display: "flex",
197
- flexDirection: "row",
198
- alignItems: "center",
199
- gap: 4,
200
- padding: "10px 12px",
201
- boxSizing: "border-box",
202
- };
203
- const bodyRowStyle: CSSProperties = {
204
- display: "flex",
205
- alignItems: "stretch",
206
- borderBottom: `1px solid ${colors.border}`,
207
- };
208
- const bodyCellStyle: CSSProperties = {
209
- minHeight: 44,
210
- display: "flex",
211
- flexDirection: "column",
212
- justifyContent: "center",
213
- padding: 12,
214
- boxSizing: "border-box",
215
- };
216
- const chevronButtonStyle: CSSProperties = {
217
- width: CHEVRON_W,
218
- flexShrink: 0,
219
- display: "flex",
220
- alignItems: "center",
221
- justifyContent: "center",
222
- background: "transparent",
223
- border: "none",
224
- padding: 0,
225
- cursor: "pointer",
226
- };
227
- const detailRowStyle: CSSProperties = {
228
- background: colors.zinc[50],
229
- borderBottom: `1px solid ${colors.border}`,
230
- };
231
- const detailCellStyle: CSSProperties = {
232
- padding: "4px 16px 18px",
233
- width: "100%",
234
- boxSizing: "border-box",
235
- };