@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 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` /
@@ -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", 38], ["northgate", 27], ["riverside", 21], ["southbay", 14]]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
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
- {items.map((item) => {
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: 10,
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: 36,
159
+ width: 30,
160
+ },
161
+ toggle: {
162
+ gap: 4,
163
+ minHeight: 32,
120
164
  },
121
165
  });