@lotics/ui 4.7.0 → 4.8.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/AGENTS.md +3 -1
- package/examples/tpl_stock.tsx +9 -1
- package/package.json +1 -1
- package/src/breakdown.tsx +48 -4
package/AGENTS.md
CHANGED
|
@@ -56,7 +56,9 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
56
56
|
with `Pagination`). Never an HTML `<table>` or a `.map` of rows.
|
|
57
57
|
- **Numbers & charts** — `KPIStrip` / `KPICard` / `Metric` (headline figures), `TrendChip`
|
|
58
58
|
(delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
|
|
59
|
-
recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown
|
|
59
|
+
recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
|
|
60
|
+
(a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
|
|
61
|
+
"Show N more" toggle — `labels` to localize — so several facet cards align to one height in a row),
|
|
60
62
|
`StatusGrid` + `StatusLegend`, `Heatmap`.
|
|
61
63
|
- **Surfaces & layout** — `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/
|
|
62
64
|
`CardFooter`), `SectionCard`, `SectionHeading` (+ `SectionHeadingTitle`), `PageHeader` /
|
package/examples/tpl_stock.tsx
CHANGED
|
@@ -56,6 +56,11 @@ const DEPOTS = [
|
|
|
56
56
|
{ key: "northgate", label: "Northgate" },
|
|
57
57
|
{ key: "riverside", label: "Riverside" },
|
|
58
58
|
{ key: "southbay", label: "Southbay" },
|
|
59
|
+
{ key: "westpoint", label: "Westpoint" },
|
|
60
|
+
{ key: "harborview", label: "Harborview" },
|
|
61
|
+
{ key: "lakeside", label: "Lakeside" },
|
|
62
|
+
{ key: "midvale", label: "Midvale" },
|
|
63
|
+
{ key: "fairlawn", label: "Fairlawn" },
|
|
59
64
|
] as const;
|
|
60
65
|
const DEPOT_RAMP = ramp("violet", DEPOTS.length);
|
|
61
66
|
|
|
@@ -101,7 +106,7 @@ const TYPE_BASE_VALUE: Record<TypeKey, number> = {
|
|
|
101
106
|
const UNITS: Unit[] = Array.from({ length: 10_482 }, (_, i) => {
|
|
102
107
|
const type = pick<TypeKey>(i, 1, [["dry40", 34], ["dry20", 26], ["hc40", 18], ["reefer", 12], ["opentop", 7], ["tank", 3]]);
|
|
103
108
|
const status = pick<StatusKey>(i, 2, [["leased", 52], ["yard", 18], ["transit", 14], ["repair", 9], ["idle", 7]]);
|
|
104
|
-
const depot = pick<DepotKey>(i, 3, [["eastport",
|
|
109
|
+
const depot = pick<DepotKey>(i, 3, [["eastport", 26], ["northgate", 19], ["riverside", 15], ["southbay", 11], ["westpoint", 9], ["harborview", 7], ["lakeside", 5], ["midvale", 4], ["fairlawn", 4]]);
|
|
105
110
|
const ageYears = 1 + (hash(i, 4) % 12);
|
|
106
111
|
const day = 1 + (hash(i, 5) % 28);
|
|
107
112
|
const month = status === "idle" ? 1 + (hash(i, 6) % 4) : 4 + (hash(i, 6) % 3);
|
|
@@ -241,10 +246,13 @@ export function TplStock() {
|
|
|
241
246
|
</CardHeaderTitle>
|
|
242
247
|
</CardHeader>
|
|
243
248
|
<View style={{ paddingHorizontal: 20, paddingVertical: 16 }}>
|
|
249
|
+
{/* maxRows caps the long tail behind a "Show N more" toggle, so a
|
|
250
|
+
many-depot facet stays the same height as its neighbours. */}
|
|
244
251
|
<Breakdown
|
|
245
252
|
items={DEPOTS.map((d, i) => ({ key: d.key, label: d.label, value: byDepot.get(d.key) ?? 0, color: DEPOT_RAMP[i] }))}
|
|
246
253
|
selectedKey={depot}
|
|
247
254
|
onSelect={setFacet(setDepot)}
|
|
255
|
+
maxRows={5}
|
|
248
256
|
/>
|
|
249
257
|
</View>
|
|
250
258
|
</Card>
|
package/package.json
CHANGED
package/src/breakdown.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { StyleSheet, View } from "react-native";
|
|
2
3
|
import { colors, withAlpha } from "./colors";
|
|
4
|
+
import { Icon } from "./icon";
|
|
3
5
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
6
|
import { StackedProgressBar } from "./stacked_progress_bar";
|
|
5
7
|
import { Text } from "./text";
|
|
@@ -12,6 +14,15 @@ export interface BreakdownItem {
|
|
|
12
14
|
color: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** Expand/collapse labels for a capped breakdown (`maxRows`). Localized
|
|
18
|
+
* downstream — the kit ships English defaults. */
|
|
19
|
+
export interface BreakdownLabels {
|
|
20
|
+
/** The expand affordance, given how many rows are currently hidden. */
|
|
21
|
+
more: (hidden: number) => string;
|
|
22
|
+
/** The collapse affordance. */
|
|
23
|
+
less: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export interface BreakdownProps {
|
|
16
27
|
items: BreakdownItem[];
|
|
17
28
|
/** The drilled-in segment. While set, the other segments dim. */
|
|
@@ -20,9 +31,17 @@ export interface BreakdownProps {
|
|
|
20
31
|
* render an informational breakdown. */
|
|
21
32
|
onSelect?: (key: string | null) => void;
|
|
22
33
|
formatValue?: (n: number) => string;
|
|
34
|
+
/** Cap the visible rows; the rest fold behind a "show more" toggle. Keeps a
|
|
35
|
+
* long-tail breakdown a fixed height (so several align in a row) while staying
|
|
36
|
+
* fully drillable on expand. The stacked bar always reflects ALL items. Omit
|
|
37
|
+
* to show every row. */
|
|
38
|
+
maxRows?: number;
|
|
39
|
+
/** Expand/collapse labels (defaults to English). */
|
|
40
|
+
labels?: BreakdownLabels;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
const defaultFormat = (n: number): string => n.toLocaleString("en-US");
|
|
44
|
+
const DEFAULT_LABELS: BreakdownLabels = { more: (hidden) => `Show ${hidden} more`, less: "Show less" };
|
|
26
45
|
|
|
27
46
|
/**
|
|
28
47
|
* Composition browser for LARGE populations — the answer to "what is my
|
|
@@ -34,8 +53,15 @@ const defaultFormat = (n: number): string => n.toLocaleString("en-US");
|
|
|
34
53
|
* full faceted overview; selections combine in the host's filter state.
|
|
35
54
|
*/
|
|
36
55
|
export function Breakdown(props: BreakdownProps) {
|
|
37
|
-
const { items, selectedKey = null, onSelect, formatValue = defaultFormat } = props;
|
|
56
|
+
const { items, selectedKey = null, onSelect, formatValue = defaultFormat, maxRows, labels = DEFAULT_LABELS } = props;
|
|
38
57
|
const total = items.reduce((sum, item) => sum + item.value, 0);
|
|
58
|
+
const [expanded, setExpanded] = useState(false);
|
|
59
|
+
// Cap the rows when `maxRows` is set and the tail is longer; the bar still
|
|
60
|
+
// reflects every item, only the ranked list folds.
|
|
61
|
+
const cap = maxRows ?? items.length;
|
|
62
|
+
const collapsible = items.length > cap;
|
|
63
|
+
const hidden = items.length - cap;
|
|
64
|
+
const visible = collapsible && !expanded ? items.slice(0, cap) : items;
|
|
39
65
|
|
|
40
66
|
return (
|
|
41
67
|
<View style={styles.container}>
|
|
@@ -49,7 +75,7 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
49
75
|
}))}
|
|
50
76
|
/>
|
|
51
77
|
<View>
|
|
52
|
-
{
|
|
78
|
+
{visible.map((item) => {
|
|
53
79
|
const share = total > 0 ? Math.round((item.value / total) * 100) : 0;
|
|
54
80
|
const selected = selectedKey === item.key;
|
|
55
81
|
const dimmed = selectedKey !== null && !selected;
|
|
@@ -84,6 +110,20 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
84
110
|
</View>
|
|
85
111
|
);
|
|
86
112
|
})}
|
|
113
|
+
{collapsible ? (
|
|
114
|
+
<PressableHighlight
|
|
115
|
+
accessibilityRole="button"
|
|
116
|
+
accessibilityState={{ expanded }}
|
|
117
|
+
accessibilityLabel={expanded ? labels.less : labels.more(hidden)}
|
|
118
|
+
onPress={() => setExpanded((e) => !e)}
|
|
119
|
+
style={[styles.row, styles.pressable, styles.toggle]}
|
|
120
|
+
>
|
|
121
|
+
<Text size="sm" weight="medium" color="muted">
|
|
122
|
+
{expanded ? labels.less : labels.more(hidden)}
|
|
123
|
+
</Text>
|
|
124
|
+
<Icon name={expanded ? "chevron-up" : "chevron-down"} size={16} color={colors.zinc[400]} />
|
|
125
|
+
</PressableHighlight>
|
|
126
|
+
) : null}
|
|
87
127
|
</View>
|
|
88
128
|
</View>
|
|
89
129
|
);
|
|
@@ -96,7 +136,7 @@ const styles = StyleSheet.create({
|
|
|
96
136
|
row: {
|
|
97
137
|
flexDirection: "row",
|
|
98
138
|
alignItems: "center",
|
|
99
|
-
gap:
|
|
139
|
+
gap: 8,
|
|
100
140
|
minHeight: 36,
|
|
101
141
|
},
|
|
102
142
|
pressable: {
|
|
@@ -116,6 +156,10 @@ const styles = StyleSheet.create({
|
|
|
116
156
|
flex: 1,
|
|
117
157
|
},
|
|
118
158
|
share: {
|
|
119
|
-
width:
|
|
159
|
+
width: 30,
|
|
160
|
+
},
|
|
161
|
+
toggle: {
|
|
162
|
+
gap: 4,
|
|
163
|
+
minHeight: 32,
|
|
120
164
|
},
|
|
121
165
|
});
|