@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.
- package/package.json +27 -8
- package/src/accordion.tsx +146 -63
- package/src/action_menu.tsx +72 -0
- package/src/allocation_row.tsx +54 -0
- package/src/avatar.web.tsx +102 -0
- package/src/badge.tsx +40 -9
- package/src/breakdown.tsx +121 -0
- package/src/card.tsx +150 -0
- package/src/cell_select.tsx +3 -2
- package/src/chip_group.tsx +65 -0
- package/src/colors.ts +61 -0
- package/src/column_filter.tsx +9 -24
- package/src/completion_state.tsx +43 -0
- package/src/control_surface.ts +32 -0
- package/src/counter.tsx +58 -0
- package/src/date_range_filter_field.tsx +44 -12
- package/src/detail_row.tsx +45 -0
- package/src/dialog.tsx +0 -24
- package/src/download.ts +2 -1
- package/src/drawer.tsx +94 -2
- package/src/empty_state.tsx +37 -0
- package/src/file_badge.tsx +27 -4
- package/src/file_dropzone.tsx +188 -0
- package/src/file_picker.ts +45 -0
- package/src/filter_pill.tsx +106 -0
- package/src/floating_action_bar.tsx +57 -0
- package/src/fonts.css +10 -13
- package/src/format_money.ts +38 -0
- package/src/heatmap.tsx +153 -0
- package/src/icon.tsx +2 -0
- package/src/icon_button.tsx +16 -2
- package/src/index.css +4 -3
- package/src/info_popover.tsx +4 -6
- package/src/kpi_card.tsx +19 -6
- package/src/kpi_strip.tsx +89 -0
- package/src/line_chart.tsx +61 -34
- package/src/link_button.tsx +50 -0
- package/src/metric.tsx +21 -12
- package/src/pagination.tsx +5 -9
- package/src/peek.tsx +68 -0
- package/src/picker.tsx +13 -1
- package/src/picker_menu.tsx +8 -16
- package/src/pie_chart.tsx +29 -8
- package/src/pill_button.tsx +10 -8
- package/src/popover.tsx +14 -4
- package/src/pressable_highlight.tsx +10 -1
- package/src/pressable_row.tsx +91 -0
- package/src/progress_bar.tsx +47 -17
- package/src/radio_picker.tsx +20 -9
- package/src/range_slider.tsx +185 -0
- package/src/remainder_meter.tsx +48 -0
- package/src/ring_gauge.tsx +5 -5
- package/src/scan_field.tsx +58 -0
- package/src/search_input.tsx +12 -0
- package/src/sort_header.tsx +102 -0
- package/src/stacked_progress_bar.tsx +51 -16
- package/src/status_grid.tsx +187 -0
- package/src/step_list.tsx +128 -0
- package/src/step_progress.tsx +145 -0
- package/src/stepper.tsx +9 -4
- package/src/table.tsx +168 -112
- package/src/text.tsx +15 -0
- package/src/text_utils.ts +10 -0
- package/src/timeline.tsx +90 -57
- package/src/trend_footer.tsx +2 -2
- package/src/alert_row.tsx +0 -81
- package/src/table.web.tsx +0 -235
- package/src/table_picker.tsx +0 -305
- package/src/table_types.ts +0 -47
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { View, type ViewStyle } from "react-native";
|
|
2
|
+
import { colors } from "./colors";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
import { useTooltip } from "./tooltip";
|
|
5
|
+
|
|
6
|
+
export interface StepProgressProps {
|
|
7
|
+
/** The stages: pass the NAMES (enables the built-in "4/7 · In" caption
|
|
8
|
+
* and per-segment hover names) or a bare count (bar only). */
|
|
9
|
+
steps: number | string[];
|
|
10
|
+
/** 0-based index of the stage in progress; earlier segments render
|
|
11
|
+
* complete. Pass -1 for "not started", `steps.length` for "complete". */
|
|
12
|
+
current: number;
|
|
13
|
+
/** Accent for the completed/current segments. Defaults to the neutral
|
|
14
|
+
* ink — pass a brand color to theme it. */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** What this progress measures ("Production stages") — adds the xs muted
|
|
17
|
+
* uppercase eyebrow row above the bar, caption right. Omit at card
|
|
18
|
+
* density where the context already names it. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Caption override for when the current stage needs prose ("In production") instead of the derived "3/6 · SX". */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Tone for the caption — `danger` flags a stalled/overdue stage (a stuck
|
|
23
|
+
* production order) in the FIXED caption spot, so the bar stays consistent
|
|
24
|
+
* instead of a floating badge shifting it. */
|
|
25
|
+
captionTone?: "default" | "danger";
|
|
26
|
+
/** Put the caption on its OWN line below a full-width bar (vs the default
|
|
27
|
+
* inline-right, which lets a varying caption shrink the bar). The compact,
|
|
28
|
+
* consistent card/drawer layout — no eyebrow, the bar never shifts. */
|
|
29
|
+
captionBelow?: boolean;
|
|
30
|
+
/** Segment height. Default 10 — matches ProgressBar's track; thinner reads
|
|
31
|
+
* weak. */
|
|
32
|
+
height?: number;
|
|
33
|
+
accessibilityLabel?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* THE compact stage indicator — N equal segments filled through the current
|
|
38
|
+
* stage, with a built-in "4/7 · In" caption when stages are named (hovering
|
|
39
|
+
* a segment names it). For *countable* stages an item walks through (a
|
|
40
|
+
* production line, a checklist, a pipeline) shown at card/list density.
|
|
41
|
+
*
|
|
42
|
+
* Picking a bar: continuous value-vs-max → `ProgressBar`; weighted
|
|
43
|
+
* composition/funnel → `StackedProgressBar`; a full-width wizard header
|
|
44
|
+
* where every milestone label must be visible → `Stepper`; everything
|
|
45
|
+
* stage-countable at card density → this.
|
|
46
|
+
*/
|
|
47
|
+
export function StepProgress(props: StepProgressProps) {
|
|
48
|
+
const { steps, current, color = colors.zinc[900], title, label, captionTone = "default", captionBelow = false, height = 10, accessibilityLabel } = props;
|
|
49
|
+
const captionColor = captionTone === "danger" ? "danger" : "muted";
|
|
50
|
+
const names = typeof steps === "number" ? null : steps;
|
|
51
|
+
const count = Math.max(1, typeof steps === "number" ? steps : steps.length);
|
|
52
|
+
const isComplete = current >= count;
|
|
53
|
+
const safe = Math.min(current, count - 1);
|
|
54
|
+
const caption =
|
|
55
|
+
label ??
|
|
56
|
+
(names
|
|
57
|
+
? isComplete
|
|
58
|
+
? `${count}/${count} · Complete`
|
|
59
|
+
: `${Math.max(0, safe + 1)}/${count}${safe >= 0 ? ` · ${names[safe]}` : ""}`
|
|
60
|
+
: undefined);
|
|
61
|
+
|
|
62
|
+
const bar = (
|
|
63
|
+
<View
|
|
64
|
+
accessibilityRole="progressbar"
|
|
65
|
+
accessibilityLabel={accessibilityLabel ?? caption ?? `${Math.max(0, safe + 1)} of ${count}`}
|
|
66
|
+
style={{ flexDirection: "row", gap: 3, flex: 1 }}
|
|
67
|
+
>
|
|
68
|
+
{Array.from({ length: count }, (_, i) => (
|
|
69
|
+
<Segment
|
|
70
|
+
key={i}
|
|
71
|
+
name={names?.[i]}
|
|
72
|
+
color={color}
|
|
73
|
+
height={height}
|
|
74
|
+
// Complete settles ALL segments solid — "finished" reads as
|
|
75
|
+
// accomplished, not faded.
|
|
76
|
+
done={i < safe}
|
|
77
|
+
isCurrent={isComplete || (i === safe && safe >= 0)}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (title) {
|
|
84
|
+
return (
|
|
85
|
+
<View style={{ gap: 6, flex: 1 }}>
|
|
86
|
+
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 12 }}>
|
|
87
|
+
<Text size="xs" color="muted" transform="uppercase">
|
|
88
|
+
{title}
|
|
89
|
+
</Text>
|
|
90
|
+
<View style={{ flex: 1 }} />
|
|
91
|
+
{caption ? (
|
|
92
|
+
<Text size="xs" color={captionColor} weight={captionTone === "danger" ? "medium" : "regular"} tabular>
|
|
93
|
+
{caption}
|
|
94
|
+
</Text>
|
|
95
|
+
) : null}
|
|
96
|
+
</View>
|
|
97
|
+
{bar}
|
|
98
|
+
</View>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!caption) return bar;
|
|
103
|
+
if (captionBelow) {
|
|
104
|
+
return (
|
|
105
|
+
<View style={{ gap: 6, flex: 1 }}>
|
|
106
|
+
{bar}
|
|
107
|
+
<Text size="xs" color={captionColor} weight={captionTone === "danger" ? "medium" : "regular"} tabular>
|
|
108
|
+
{caption}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return (
|
|
114
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, flex: 1 }}>
|
|
115
|
+
{bar}
|
|
116
|
+
<Text size="xs" color={captionColor} weight={captionTone === "danger" ? "medium" : "regular"} tabular>
|
|
117
|
+
{caption}
|
|
118
|
+
</Text>
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Segment(props: { name?: string; color: string; height: number; done: boolean; isCurrent: boolean }) {
|
|
124
|
+
const { name, color, height, done, isCurrent } = props;
|
|
125
|
+
// Hovering a segment names its stage — the bar stays self-explanatory
|
|
126
|
+
// even where the caption is elided. No-ops without a TooltipProvider.
|
|
127
|
+
const tooltip = useTooltip(name);
|
|
128
|
+
return (
|
|
129
|
+
<View
|
|
130
|
+
{...tooltip}
|
|
131
|
+
style={{
|
|
132
|
+
flex: 1,
|
|
133
|
+
height,
|
|
134
|
+
borderRadius: height / 2,
|
|
135
|
+
backgroundColor: done || isCurrent ? color : colors.zinc[100],
|
|
136
|
+
opacity: done && !isCurrent ? 0.4 : 1,
|
|
137
|
+
...(done || isCurrent
|
|
138
|
+
? null
|
|
139
|
+
: ({ boxShadow: "inset 0 0 0 1px rgba(38,38,38,0.04)" } as ViewStyle)),
|
|
140
|
+
// Progress that snaps is dead; progress that moves is alive.
|
|
141
|
+
...({ transition: "background-color 200ms ease-out, opacity 200ms ease-out" } as ViewStyle),
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
package/src/stepper.tsx
CHANGED
|
@@ -23,18 +23,23 @@ function statusOf(index: number, current: number): StepStatus {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
* current, and upcoming states and a connecting track.
|
|
28
|
-
*
|
|
26
|
+
* Full-width wizard/milestone header — a row of labeled milestones with
|
|
27
|
+
* completed, current, and upcoming states and a connecting track. Every
|
|
28
|
+
* label is visible; use it where the journey itself is the headline (a
|
|
29
|
+
* record detail, a checkout). At card/list density use `StepProgress`
|
|
30
|
+
* (segments + built-in caption) instead. For a vertical event log use
|
|
31
|
+
* `Timeline`.
|
|
29
32
|
*/
|
|
30
33
|
export function Stepper(props: StepperProps) {
|
|
31
34
|
const { steps, current, color = colors.zinc[900], accessibilityLabel } = props;
|
|
32
35
|
const last = steps.length - 1;
|
|
33
36
|
const safe = Math.max(0, Math.min(current, last));
|
|
37
|
+
const a11y = accessibilityLabel ?? `Step ${safe + 1} of ${steps.length}: ${steps[safe] ?? ""}`;
|
|
38
|
+
|
|
34
39
|
return (
|
|
35
40
|
<View
|
|
36
41
|
accessibilityRole="progressbar"
|
|
37
|
-
accessibilityLabel={
|
|
42
|
+
accessibilityLabel={a11y}
|
|
38
43
|
style={{ flexDirection: "row" }}
|
|
39
44
|
>
|
|
40
45
|
{steps.map((label, i) => {
|
package/src/table.tsx
CHANGED
|
@@ -1,125 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
isValidElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { StyleSheet, View, Pressable, type ViewStyle } from "react-native";
|
|
11
|
+
import { Text } from "./text";
|
|
12
|
+
import { Divider } from "./divider";
|
|
13
|
+
import { PressableRow } from "./pressable_row";
|
|
14
|
+
import { SortHeader, type SortState } from "./sort_header";
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
/**
|
|
17
|
+
* One column of a register — its width/flex/align/label/sortability defined ONCE,
|
|
18
|
+
* here, instead of being re-typed in the header band AND every row.
|
|
19
|
+
*/
|
|
20
|
+
export interface TableColumn {
|
|
21
|
+
/** Stable id — also the `sortKey` when `sortable`. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** Header label (uppercase eyebrow). Omit for a control column (a trailing ⋯). */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Fixed width in px; omit for a flexible column. */
|
|
26
|
+
width?: number;
|
|
27
|
+
/** Flex grow when no `width` (default 1). */
|
|
28
|
+
flex?: number;
|
|
29
|
+
align?: "left" | "right";
|
|
30
|
+
sortable?: boolean;
|
|
31
|
+
}
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
interface TableCtx {
|
|
34
|
+
columns: TableColumn[];
|
|
35
|
+
leading: number;
|
|
36
|
+
trailing: number;
|
|
37
|
+
}
|
|
38
|
+
const TableContext = createContext<TableCtx | null>(null);
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
function colStyle(col: TableColumn): ViewStyle {
|
|
41
|
+
const base: ViewStyle =
|
|
42
|
+
col.width != null ? { width: col.width } : { flex: col.flex ?? 1, minWidth: 0 };
|
|
43
|
+
if (col.align === "right") base.alignItems = "flex-end";
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
export interface TableProps {
|
|
48
|
+
/** The single source of column layout — shared by the header AND every cell. */
|
|
49
|
+
columns: TableColumn[];
|
|
50
|
+
sort?: SortState | null;
|
|
51
|
+
/** Cycles the sort (none→asc→desc→none) — pair with `cycleSort` in the parent. */
|
|
52
|
+
onSort?: (key: string) => void;
|
|
53
|
+
/** Reserve a leading gutter (px) for rows that render a `leading` slot (a checkbox). */
|
|
54
|
+
leading?: number;
|
|
55
|
+
/** Reserve a trailing gutter (px) for rows that render a `trailing` slot (a ⋯ / button). */
|
|
56
|
+
trailing?: number;
|
|
57
|
+
/** The `TableRow`s. */
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}
|
|
35
60
|
|
|
36
|
-
|
|
61
|
+
/**
|
|
62
|
+
* THE columnar register — define `columns` once and the header band + every row's
|
|
63
|
+
* cell widths come from it, so they can't drift (no hand-rolled `W` map, no
|
|
64
|
+
* `<View style={{width}}>` per cell). Renders a full-bleed eyebrow header (a
|
|
65
|
+
* sortable column becomes a `SortHeader`) and its `TableRow` children,
|
|
66
|
+
* `Divider`-separated. Compose `TableRow` / `TableCell` for the body. For a
|
|
67
|
+
* non-columnar list (entity piles, card stacks) use `PressableRow` directly.
|
|
68
|
+
*/
|
|
69
|
+
export function Table(props: TableProps) {
|
|
70
|
+
const { columns, sort, onSort, leading = 0, trailing = 0, children } = props;
|
|
71
|
+
const rows = Children.toArray(children).filter(isValidElement);
|
|
37
72
|
|
|
38
73
|
return (
|
|
39
|
-
<
|
|
40
|
-
<View style={styles.
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
size={14}
|
|
54
|
-
color={active ? colors.zinc[700] : colors.zinc[400]}
|
|
55
|
-
/>
|
|
56
|
-
) : null}
|
|
57
|
-
</View>
|
|
58
|
-
);
|
|
59
|
-
return sortable ? (
|
|
60
|
-
<Pressable key={col.key as string} accessibilityRole="button" accessibilityLabel={`Sắp xếp theo ${col.label}`} onPress={() => onSortChange?.(col.key)} style={cellStyle}>
|
|
61
|
-
{inner}
|
|
62
|
-
</Pressable>
|
|
63
|
-
) : (
|
|
64
|
-
<View key={col.key as string} style={cellStyle}>
|
|
65
|
-
{inner}
|
|
66
|
-
</View>
|
|
67
|
-
);
|
|
68
|
-
})}
|
|
69
|
-
{expandable ? <View style={{ width: CHEVRON_W }} /> : null}
|
|
70
|
-
</View>
|
|
71
|
-
{rows.filter((r): r is TRow => Boolean(r)).map((row, i) => {
|
|
72
|
-
const key = rowKey ? String(row[rowKey]) : String(i);
|
|
73
|
-
const isOpen = expanded.has(key);
|
|
74
|
-
const extra = rowStyle?.(row);
|
|
75
|
-
const cells = columns.map((col) => (
|
|
76
|
-
<View key={col.key as string} style={[cellWidth(col), styles.bodyCell, col.align === "right" ? styles.alignEnd : null]}>
|
|
77
|
-
{col.renderCell ? col.renderCell({ row, column: col }) : <Text numberOfLines={1}>{row[col.key] != null ? String(row[col.key]) : ""}</Text>}
|
|
74
|
+
<TableContext.Provider value={{ columns, leading, trailing }}>
|
|
75
|
+
<View style={styles.headerBand}>
|
|
76
|
+
{leading > 0 ? <View style={{ width: leading }} /> : null}
|
|
77
|
+
{columns.map((col) => (
|
|
78
|
+
<View key={col.key} style={colStyle(col)}>
|
|
79
|
+
{col.label ? (
|
|
80
|
+
col.sortable && onSort ? (
|
|
81
|
+
<SortHeader label={col.label} sortKey={col.key} sort={sort ?? null} onSort={onSort} align={col.align} />
|
|
82
|
+
) : (
|
|
83
|
+
<Text size="xs" color="muted" transform="uppercase" numberOfLines={1}>
|
|
84
|
+
{col.label}
|
|
85
|
+
</Text>
|
|
86
|
+
)
|
|
87
|
+
) : null}
|
|
78
88
|
</View>
|
|
79
|
-
))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{cells}
|
|
90
|
-
<View style={styles.chevron}>
|
|
91
|
-
<Icon name={isOpen ? "chevron-up" : "chevron-down"} size={18} color={colors.zinc[400]} />
|
|
92
|
-
</View>
|
|
93
|
-
</Pressable>
|
|
94
|
-
) : onRowPress ? (
|
|
95
|
-
<Pressable
|
|
96
|
-
accessibilityRole="button"
|
|
97
|
-
onPress={() => onRowPress(row)}
|
|
98
|
-
style={({ pressed }) => [styles.bodyRow, pressed ? styles.rowPressed : null, extra]}
|
|
99
|
-
>
|
|
100
|
-
{cells}
|
|
101
|
-
</Pressable>
|
|
102
|
-
) : (
|
|
103
|
-
<View style={[styles.bodyRow, extra]}>{cells}</View>
|
|
104
|
-
)}
|
|
105
|
-
{expandable && isOpen ? <View style={styles.detail}>{renderDetail!(row)}</View> : null}
|
|
106
|
-
</Fragment>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
</ScrollView>
|
|
89
|
+
))}
|
|
90
|
+
{trailing > 0 ? <View style={{ width: trailing }} /> : null}
|
|
91
|
+
</View>
|
|
92
|
+
{rows.map((row, i) => (
|
|
93
|
+
<View key={(row as ReactElement).key ?? i}>
|
|
94
|
+
<Divider />
|
|
95
|
+
{row}
|
|
96
|
+
</View>
|
|
97
|
+
))}
|
|
98
|
+
</TableContext.Provider>
|
|
110
99
|
);
|
|
111
100
|
}
|
|
112
101
|
|
|
102
|
+
export interface TableRowProps {
|
|
103
|
+
/** Opens the record (the drawer). The door + the full-row surface both call it. */
|
|
104
|
+
onPress?: () => void;
|
|
105
|
+
selected?: boolean;
|
|
106
|
+
/** Accessible name for the door (the keyboard-focusable open target). */
|
|
107
|
+
accessibilityLabel?: string;
|
|
108
|
+
/** Outside the door, BEFORE the cells (a selection checkbox) — width = Table `leading`. */
|
|
109
|
+
leading?: ReactNode;
|
|
110
|
+
/** Outside the door, AFTER the cells (a ⋯ menu / a button) — width = Table `trailing`. */
|
|
111
|
+
trailing?: ReactNode;
|
|
112
|
+
/** Min row height. Default 52. */
|
|
113
|
+
minHeight?: number;
|
|
114
|
+
/** The `TableCell`s, one per column, in column order. */
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A register row: a full-bleed `PressableRow` (its hover wash spans the whole row,
|
|
120
|
+
* incl. the leading/trailing controls) wrapping the door — an inner
|
|
121
|
+
* `role="button"` `Pressable` (the keyboard-accessible open) around the cells —
|
|
122
|
+
* with optional `leading`/`trailing` slots OUTSIDE the door so their own presses
|
|
123
|
+
* win. Cells map to `Table`'s columns by position.
|
|
124
|
+
*/
|
|
125
|
+
export function TableRow(props: TableRowProps) {
|
|
126
|
+
const { onPress, selected, accessibilityLabel, leading, trailing, minHeight = 52, children } = props;
|
|
127
|
+
const ctx = useContext(TableContext);
|
|
128
|
+
if (!ctx) throw new Error("TableRow must be used within a Table");
|
|
129
|
+
|
|
130
|
+
const cells = (Children.toArray(children).filter(isValidElement) as ReactElement<TableCellProps>[]).map(
|
|
131
|
+
(cell, i) => cloneElement(cell, { _column: ctx.columns[i] }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<PressableRow onPress={onPress ?? noop} selected={selected} style={styles.row}>
|
|
136
|
+
{ctx.leading > 0 ? <View style={{ width: ctx.leading }}>{leading}</View> : null}
|
|
137
|
+
<Pressable
|
|
138
|
+
accessibilityRole={onPress ? "button" : undefined}
|
|
139
|
+
accessibilityLabel={accessibilityLabel}
|
|
140
|
+
onPress={onPress}
|
|
141
|
+
style={[styles.door, { minHeight }]}
|
|
142
|
+
>
|
|
143
|
+
{cells}
|
|
144
|
+
</Pressable>
|
|
145
|
+
{ctx.trailing > 0 ? <View style={{ width: ctx.trailing, alignItems: "flex-end" }}>{trailing}</View> : null}
|
|
146
|
+
</PressableRow>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface TableCellProps {
|
|
151
|
+
children: ReactNode;
|
|
152
|
+
/** @internal — injected by `TableRow` from the column at this cell's position. */
|
|
153
|
+
_column?: TableColumn;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One cell — its width/align come from the column `TableRow` injects by position. */
|
|
157
|
+
export function TableCell(props: TableCellProps) {
|
|
158
|
+
const { children, _column } = props;
|
|
159
|
+
return <View style={_column ? colStyle(_column) : undefined}>{children}</View>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const noop = () => {};
|
|
163
|
+
|
|
113
164
|
const styles = StyleSheet.create({
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
headerBand: {
|
|
166
|
+
paddingHorizontal: 20,
|
|
167
|
+
paddingVertical: 10,
|
|
168
|
+
flexDirection: "row",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
gap: 14,
|
|
171
|
+
},
|
|
172
|
+
row: {
|
|
173
|
+
gap: 14,
|
|
174
|
+
},
|
|
175
|
+
door: {
|
|
176
|
+
flex: 1,
|
|
177
|
+
flexDirection: "row",
|
|
178
|
+
alignItems: "center",
|
|
179
|
+
gap: 14,
|
|
180
|
+
},
|
|
125
181
|
});
|
package/src/text.tsx
CHANGED
|
@@ -20,6 +20,12 @@ export interface TextProps {
|
|
|
20
20
|
userSelect?: TextUserSelect;
|
|
21
21
|
transform?: TextTransform;
|
|
22
22
|
decoration?: TextDecorationLine;
|
|
23
|
+
/**
|
|
24
|
+
* Fixed-width (tabular) numerals. Set on every free-standing numeral —
|
|
25
|
+
* table cells, counts, money, dates — so digits align across rows and
|
|
26
|
+
* columns instead of jittering on proportional glyphs.
|
|
27
|
+
*/
|
|
28
|
+
tabular?: boolean;
|
|
23
29
|
style?: RNTextProps["style"];
|
|
24
30
|
onPress?: () => void;
|
|
25
31
|
/** ID used to target this element from `accessibilityLabelledBy` / `aria-describedby`. */
|
|
@@ -62,6 +68,7 @@ export function Text(props: TextProps) {
|
|
|
62
68
|
weight = "regular",
|
|
63
69
|
numberOfLines,
|
|
64
70
|
decoration,
|
|
71
|
+
tabular,
|
|
65
72
|
transform,
|
|
66
73
|
onPress,
|
|
67
74
|
style,
|
|
@@ -99,6 +106,7 @@ export function Text(props: TextProps) {
|
|
|
99
106
|
styles[userSelect],
|
|
100
107
|
styles[align],
|
|
101
108
|
decoration && styles[decoration],
|
|
109
|
+
tabular && styles.tabular,
|
|
102
110
|
transform && styles[transform],
|
|
103
111
|
style,
|
|
104
112
|
]}
|
|
@@ -186,9 +194,16 @@ const styles = StyleSheet.create({
|
|
|
186
194
|
userSelect: "none",
|
|
187
195
|
},
|
|
188
196
|
|
|
197
|
+
tabular: {
|
|
198
|
+
fontVariant: ["tabular-nums"],
|
|
199
|
+
},
|
|
200
|
+
|
|
189
201
|
// Text transform styles
|
|
190
202
|
uppercase: {
|
|
191
203
|
textTransform: "uppercase",
|
|
204
|
+
// Uppercase needs positive tracking to read as designed, not shouted —
|
|
205
|
+
// the standard micro-cap/eyebrow treatment (stat labels, column headers).
|
|
206
|
+
letterSpacing: 0.6,
|
|
192
207
|
},
|
|
193
208
|
lowercase: {
|
|
194
209
|
textTransform: "lowercase",
|
package/src/text_utils.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type TextColor =
|
|
|
6
6
|
| "muted"
|
|
7
7
|
| "inverted"
|
|
8
8
|
| "danger"
|
|
9
|
+
| "warning"
|
|
10
|
+
| "success"
|
|
9
11
|
| "zinc-900"
|
|
10
12
|
| "zinc-700"
|
|
11
13
|
| "zinc-500";
|
|
@@ -24,8 +26,16 @@ export function getTextColor(color?: TextColor): string {
|
|
|
24
26
|
return colors.zinc["500"];
|
|
25
27
|
case "inverted":
|
|
26
28
|
return colors.white;
|
|
29
|
+
// Valence set — the lightest status weight: a state WORD whose meaning is in
|
|
30
|
+
// the word, colored for reinforcement (never color-only). Red sits darker at
|
|
31
|
+
// 900; green/amber use 700 to stay recognizably their hue while clearing AA on
|
|
32
|
+
// white (600 would fail).
|
|
27
33
|
case "danger":
|
|
28
34
|
return colors.red["900"];
|
|
35
|
+
case "warning":
|
|
36
|
+
return colors.amber["700"];
|
|
37
|
+
case "success":
|
|
38
|
+
return colors.emerald["700"];
|
|
29
39
|
default:
|
|
30
40
|
return colors.zinc[900];
|
|
31
41
|
}
|