@lotics/ui 47.5.2 → 47.6.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/docs/catalog.md CHANGED
@@ -389,7 +389,10 @@ Worked example: [`tpl_board`](../examples/tpl_board.tsx).
389
389
  `TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
390
390
  set — no recharts), `ProgressRing`, `ProgressBar` (its `compact` prop = ONE row, track + a
391
391
  plain sm tabular count beside it — the cell/heading/peek-trigger meter. **The track clamps at
392
- 100%, the caption does not** — over its max it reads `2,100 / 2,000 (105%)`. Numbers format in the
392
+ 100%, the caption does not** — over its max it reads `2,100 / 2,000 (105%)`. **`max: null` is an
393
+ unbounded meter** — the caption states the value and the caller's `unboundedLabel`
394
+ (`1,250 (no limit)`) and the track stays empty, so a capped and an uncapped allowance side by
395
+ side stay one widget rather than a bar beside a hand-rolled lookalike. Numbers format in the
393
396
  reader's locale, so
394
397
  never hand-format the value you pass in — when display precision differs from the true value
395
398
  (whole credits off a fractional balance), `formatValue` reshapes the caption text and leaves
@@ -1028,7 +1031,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1028
1031
  - **`switcher`** — `Switcher`: current-item trigger opening a popover of `MenuButton` items
1029
1032
  (`SwitcherItem { id, label }`, `currentId`, `onSelect`) — the compact entity/workspace
1030
1033
  switcher.
1031
- - **`count`** — `Count`: a small circular count bubble (`color` highlight|muted|red).
1034
+ - **`count`** — `Count`: a 20px count disc that grows into a pill past one digit (`color` highlight|muted|red).
1032
1035
  - **`shortcut_badge`** — `ShortcutBadge`: the keycap hint pill — a zinc-50 badge rendering a
1033
1036
  shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on
1034
1037
  small screens. `TextInputField shortcut` renders it built-in, while the field is EMPTY —
@@ -2011,7 +2014,7 @@ component rather than showing it at zero.
2011
2014
  sizes the fixed label column (default 80, which fits a date and not a company name), and each
2012
2015
  item's **`leading`** slot carries that entity's own mark ahead of its name.
2013
2016
  - **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
2014
- plain sm tabular count beside it.
2017
+ plain sm tabular count beside it; `max: null` + `unboundedLabel` = the same meter with no bound.
2015
2018
  - **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one. Same API shape as the bar — a real `value`/`max` rather than a
2016
2019
  pre-computed percentage, optional `label`/`caption`, and a `format` that turns the centre
2017
2020
  figure off. `format="none"` with no label leaves the arc bare at any size: that is the
package/docs/reviewing.md CHANGED
@@ -137,6 +137,11 @@ rule behind it lives in the area doc named beside it; this file never restates o
137
137
  - **Same shape → same treatment.** Group every element by its visual SHAPE (label-over-value,
138
138
  figure+caption, icon+row, chip) and check each group has ONE treatment. A reader parses shape
139
139
  before meaning, so an unexplained difference reads as an accident however principled the reason.
140
+ **The source-side signature is a branch that renders a kit component on one arm and a
141
+ hand-built lookalike on the other** — a meter with no bound, a row with no value — written to
142
+ match the primitive on the day it was built. The primitive moves and the copy does not, and the
143
+ two arms never render side by side on the fixture that was screenshotted, so measure a screen
144
+ where BOTH arms show. The fix is the missing state on the primitive, never a closer copy.
140
145
  - **Singletons.** A rung/weight/ink combination appearing ONCE either wants siblings or wants to
141
146
  join an existing class. Pick the sibling set by STRUCTURAL ROLE, not visual resemblance.
142
147
  - **Count DISTINCT rungs on the surface, then name the role each one carries.** More rungs than
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "47.5.2",
3
+ "version": "47.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./accordion": "./src/accordion.tsx",
package/src/count.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Text } from "./text";
2
- import { View } from "react-native";
2
+ import { View, StyleSheet } from "react-native";
3
3
  import { colors } from "./colors";
4
4
 
5
5
  interface CountProps {
@@ -7,28 +7,33 @@ interface CountProps {
7
7
  color?: "highlight" | "muted" | "red";
8
8
  }
9
9
 
10
+ /**
11
+ * A count in a 20px disc that grows into a pill past one digit. `xs` because its line box (18)
12
+ * is the only body rung that fits inside the disc — `sm` is 24 tall, taller than the disc, so
13
+ * the digit was clipped and sat off-centre.
14
+ */
10
15
  export function Count(props: CountProps) {
11
16
  const { count, color = "muted" } = props;
12
17
 
13
18
  return (
14
19
  <View
15
- style={{
16
- width: 20,
17
- height: 20,
18
- justifyContent: "center",
19
- alignItems: "center",
20
- borderRadius: 999,
21
- backgroundColor:
22
- color === "highlight"
23
- ? colors.zinc["700"]
24
- : color === "red"
25
- ? colors.red["700"]
26
- : undefined,
27
- }}
20
+ style={[
21
+ styles.pill,
22
+ {
23
+ backgroundColor:
24
+ color === "highlight"
25
+ ? colors.zinc["700"]
26
+ : color === "red"
27
+ ? colors.red["700"]
28
+ : undefined,
29
+ },
30
+ ]}
28
31
  >
29
32
  <Text
30
33
  numberOfLines={1}
31
- size="sm"
34
+ size="xs"
35
+ weight="medium"
36
+ tabular
32
37
  color={color === "muted" ? "zinc-500" : "inverted"}
33
38
  userSelect="none"
34
39
  >
@@ -37,3 +42,14 @@ export function Count(props: CountProps) {
37
42
  </View>
38
43
  );
39
44
  }
45
+
46
+ const styles = StyleSheet.create({
47
+ pill: {
48
+ minWidth: 20,
49
+ height: 20,
50
+ paddingHorizontal: 6,
51
+ justifyContent: "center",
52
+ alignItems: "center",
53
+ borderRadius: 999,
54
+ },
55
+ });
@@ -8,15 +8,20 @@ export type ProgressBarFormat = "percentage" | "fraction" | "none";
8
8
 
9
9
  export interface ProgressBarProps {
10
10
  value: number;
11
- max: number;
12
- /** What this bar measures ("Đã nhận", "Tiến độ giao") the xs muted
13
- * uppercase eyebrow above the bar. Omit inside a band that already names
14
- * it. */
11
+ /** `null` = no bound. The caption states the value and `unboundedLabel` in place of a
12
+ * fraction and the track stays empty, so a capped and an uncapped allowance side by side
13
+ * read as one widget. */
14
+ max: number | null;
15
+ /** What this bar measures ("Đã nhận", "Tiến độ giao") — the `Eyebrow` above the
16
+ * bar. Omit inside a band that already names it. */
15
17
  title?: string;
16
18
  /** Caption above-right of the bar: `percentage` → "50%", `fraction` →
17
19
  * "1,250 / 2,500 (50%)" (separators follow the reader's locale). Reports the
18
20
  * TRUE ratio — over `max` it reads "105%" while the track stays clamped. */
19
21
  format?: ProgressBarFormat;
22
+ /** The qualifier beside the value when `max` is null — "1,250 (no limit)". The kit carries
23
+ * no i18n, so the caller supplies the words; without them the caption is the bare value. */
24
+ unboundedLabel?: string;
20
25
  /**
21
26
  * How `value` and `max` render inside the caption. Defaults to the reader's locale grouping.
22
27
  *
@@ -61,6 +66,7 @@ export function ProgressBar(props: ProgressBarProps) {
61
66
  format = "percentage",
62
67
  formatValue,
63
68
  formatPercent,
69
+ unboundedLabel,
64
70
  color = colors.blue["500"],
65
71
  completeColor = colors.green["500"],
66
72
  compact = false,
@@ -76,17 +82,18 @@ export function ProgressBar(props: ProgressBarProps) {
76
82
  * never exceeds its max, so this only diverges for the over-allowance case, where the true
77
83
  * figure is the whole point.
78
84
  */
79
- const ratio = max > 0 ? Math.max(0, (value / max) * 100) : 0;
85
+ const ratio = max !== null && max > 0 ? Math.max(0, (value / max) * 100) : 0;
80
86
  const percentage = Math.min(100, ratio);
81
87
  const isComplete = ratio >= 100;
82
88
  const num = formatValue ?? ((n: number) => n.toLocaleString(localeTag));
83
89
  const pct = formatPercent
84
90
  ? (r: number) => formatPercent(r / 100)
85
91
  : (r: number) => `${Math.round(r)}%`;
92
+ const unbounded = unboundedLabel ? `${num(value)} (${unboundedLabel})` : num(value);
86
93
 
87
94
  if (compact) {
88
95
  const label =
89
- format === "percentage" ? pct(ratio) : `${num(value)}/${num(max)}`;
96
+ max === null ? unbounded : format === "percentage" ? pct(ratio) : `${num(value)}/${num(max)}`;
90
97
  return (
91
98
  <View style={styles.compactRow}>
92
99
  <View style={[styles.track, styles.compactTrack]}>
@@ -109,11 +116,13 @@ export function ProgressBar(props: ProgressBarProps) {
109
116
  }
110
117
 
111
118
  const caption =
112
- format === "fraction"
113
- ? `${num(value)} / ${num(max)} (${pct(ratio)})`
114
- : format === "percentage"
115
- ? pct(ratio)
116
- : null;
119
+ format === "none"
120
+ ? null
121
+ : max === null
122
+ ? unbounded
123
+ : format === "fraction"
124
+ ? `${num(value)} / ${num(max)} (${pct(ratio)})`
125
+ : pct(ratio);
117
126
 
118
127
  return (
119
128
  <View style={styles.container}>