@lotics/ui 44.3.0 → 44.4.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/MIGRATION.md CHANGED
@@ -4,6 +4,29 @@ Breaking changes, newest first — normally per major, plus the rare minor that
4
4
  anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
5
  this file exists only to move an app from one release to the next.
6
6
 
7
+ ## 44.4.0 — `MemberChip`'s `marker` is gone
8
+
9
+ A minor that broke a type, recorded here under its exact version rather than held
10
+ for a major: `marker` existed for one release, had a single consumer inside the
11
+ kit, and underlined the chip's name to say "this opens something".
12
+
13
+ Delete the prop. A chip that opens something is a `MemberPeek`, which now carries
14
+ its own affordance through the trigger's control-band surface — the underline was
15
+ decorating the text to describe the container.
16
+
17
+ ```tsx
18
+ // before
19
+ <MemberChip name={m.name} marker />
20
+
21
+ // after
22
+ <MemberPeek name={m.name} identity={m.email} groups={m.groups} />
23
+ ```
24
+
25
+ A chip carrying `secondary` also takes a larger mark now (`lg` rather than the
26
+ requested `md`), because two lines of text stand taller than a 28px avatar. Single
27
+ -line chips are unchanged. Nothing to do — but a layout that hard-coded 28px
28
+ beside a two-line chip will need the number.
29
+
7
30
  ## 44.0.0 — `Finding` is horizontal, and `FindingComparison` is gone
8
31
 
9
32
  `Finding` stacked six blocks — badge, title, detail, a labelled row per side, a hairline, the
package/docs/catalog.md CHANGED
@@ -1295,9 +1295,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1295
1295
  reference is a FIELD, not a section — one pointer to one other row never earns a heading
1296
1296
  and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
1297
1297
  (customer) and its Transport parties.
1298
- - **`peek`** — `Peek`: drill-down for inline references — press a name/id where it appears
1299
- and get its details in an anchored popover, without leaving the screen; keep the content a
1300
- summary with ONE action to the full record.
1298
+ - **`peek`** — `Peek`: drill-down for a reference — press a name/id where it appears and get its
1299
+ details in an anchored popover, without leaving the screen; keep the content a summary with ONE
1300
+ action to the full record. **`variant` picks the register the TRIGGER sits in, and getting it
1301
+ wrong is visible:** `text` (default) boxes the child at the 32px chip scale and bleeds the extra
1302
+ height back out, so a reference inside a line of reading never grows that line; `control` takes
1303
+ the full `CONTROL_HEIGHT` band and the control radius, for a token that is a control-band citizen
1304
+ — anything built on `MemberChip`, whose `md` avatar is sized to `CONTROL_CONTENT_HEIGHT` for
1305
+ exactly that. On the wrong one a chip under-fills the band and stands a different height from the
1306
+ same chip un-peeked beside it. The horizontal bleed is common to both, so a peekable token keeps
1307
+ the surface's left edge.
1301
1308
 
1302
1309
  ### Lists, tables & registers
1303
1310
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "44.3.0",
3
+ "version": "44.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { View, StyleSheet, StyleProp, ViewStyle } from "react-native";
3
3
  import { Avatar } from "./avatar";
4
4
  import { Text } from "./text";
5
- import type { AvatarSize } from "./avatar_size";
5
+ import { AVATAR_PX, type AvatarSize } from "./avatar_size";
6
6
  import { useLoticsLocale } from "./locale";
7
7
 
8
8
  /**
@@ -34,22 +34,6 @@ interface MemberChipProps {
34
34
  * than as a current assignment. The chip keeps its height, because its usual
35
35
  * home is a fixed row: this is a colour change, never an extra line. */
36
36
  inactive?: boolean;
37
- /**
38
- * This chip is a DOOR — something opens when it is pressed. Underlines the
39
- * name in neutral ink, the kit's marker for "leads somewhere, and not away"
40
- * (see `TextLink` with no `href`).
41
- *
42
- * It exists because a pressable that paints nothing at rest is text: with no
43
- * ground, no border and no underline, its only affordance is a hover wash,
44
- * which does not exist on touch and has not happened yet for anyone who has
45
- * not already pointed at it. The chip then measures perfectly, carries a real
46
- * focus ring, and is never pressed.
47
- *
48
- * Set it ONLY when the chip itself opens something. A chip inside a row that
49
- * navigates must not wear it — the row is the door, and marking the chip
50
- * promises a second destination that is not there.
51
- */
52
- marker?: boolean;
53
37
  style?: StyleProp<ViewStyle>;
54
38
  }
55
39
 
@@ -75,7 +59,6 @@ export function MemberChip({
75
59
  secondary,
76
60
  size = "md",
77
61
  inactive,
78
- marker,
79
62
  style,
80
63
  }: MemberChipProps) {
81
64
  // `Avatar` has always taken this word from the pack; the NAME beside it was a
@@ -105,9 +88,20 @@ export function MemberChip({
105
88
  // name is not a pair, and leaving it on prose leading keeps every single-line
106
89
  // chip — every picker option, every cell — pixel-identical to before.
107
90
  const pairLeading = secondary ? ("tight" as const) : undefined;
91
+ // THE MARK IS NEVER SHORTER THAN WHAT IT MARKS. Two tight lines measure ~38
92
+ // at this rung, so beside a 28px avatar the text overflowed the mark by ten
93
+ // pixels and the person's face read as a decoration hung off their name.
94
+ // Tight leading is already the floor — 38 is as short as the pair gets — so
95
+ // the fix is the MARK, not the type: a chip carrying a supporting line is a
96
+ // taller object, and it takes the rung that covers the stack. That rung is
97
+ // `lg` = `CONTROL_HEIGHT`, which is the band a two-line chip belongs in
98
+ // anyway. A caller already above it (`xl`, a profile header) keeps what it
99
+ // asked for.
100
+ const markSize: AvatarSize =
101
+ secondary && AVATAR_PX[size] < AVATAR_PX.lg ? "lg" : size;
108
102
  return (
109
103
  <View style={[styles.row, style]}>
110
- <Avatar size={size} name={displayName} source={image ? { uri: image } : undefined} />
104
+ <Avatar size={markSize} name={displayName} source={image ? { uri: image } : undefined} />
111
105
  <View style={styles.text}>
112
106
  <Text
113
107
  userSelect="none"
@@ -118,10 +112,6 @@ export function MemberChip({
118
112
  // supporting line except by colour.
119
113
  weight="medium"
120
114
  leading={pairLeading}
121
- // NEUTRAL underline, never the navigation blue: pressing this opens
122
- // the person in place rather than travelling to them, and blue
123
- // underlined text promises a destination.
124
- decoration={marker ? "underline" : undefined}
125
115
  color={inactive ? "zinc-500" : undefined}
126
116
  numberOfLines={1}
127
117
  >
@@ -72,6 +72,12 @@ export function MemberPeek(props: MemberPeekProps) {
72
72
  <Peek
73
73
  accessibilityLabel={accessibilityLabel ?? words.profile(displayName)}
74
74
  content={<MemberProfileCard {...card} />}
75
+ // A chip is a CONTROL-BAND citizen — `md` is `CONTROL_CONTENT_HEIGHT`
76
+ // precisely so it seats in one. On the default `text` register the
77
+ // trigger boxed it at 32 and bled 4 off each side, so a peeking chip
78
+ // under-filled the band and stood shorter than the same chip un-peeked
79
+ // beside it.
80
+ variant="control"
75
81
  side={side}
76
82
  align={align}
77
83
  >
@@ -81,9 +87,6 @@ export function MemberPeek(props: MemberPeekProps) {
81
87
  secondary={secondary}
82
88
  size={size}
83
89
  inactive={card.inactive}
84
- // This chip IS the door — without the marker it paints nothing at rest
85
- // and only a pointer already on it would ever learn that.
86
- marker
87
90
  style={style}
88
91
  />
89
92
  </Peek>
package/src/peek.tsx CHANGED
@@ -3,6 +3,7 @@ import { StyleSheet } from "react-native";
3
3
  import { Popover, PopoverTrigger, PopoverContent } from "./popover";
4
4
  import type { PopoverSide, PopoverAlign } from "./popover";
5
5
  import { PressableHighlight } from "./pressable_highlight";
6
+ import { CONTROL_HEIGHT, CONTROL_RADIUS } from "./control_surface";
6
7
 
7
8
  export interface PeekProps {
8
9
  /** The inline reference that becomes the trigger — a customer name, a
@@ -15,6 +16,27 @@ export interface PeekProps {
15
16
  content: ReactNode;
16
17
  /** Announced name for the trigger ("Hồ sơ KOMASPEC VIỆT NAM"). */
17
18
  accessibilityLabel: string;
19
+ /**
20
+ * WHICH REGISTER the trigger sits in — the two surfaces a peek lives on need
21
+ * different boxes, and picking by taste puts the wrong one on both.
22
+ *
23
+ * `text` (default) is a reference INSIDE a line of reading — a customer name
24
+ * in a sentence, an id in a table cell. It takes the 32px chip register and
25
+ * bleeds the extra height back out with negative margins, so wrapping a
26
+ * `Text` never grows the line it sits in.
27
+ *
28
+ * `control` is a reference that IS a control-band citizen — anything built on
29
+ * `MemberChip`, whose `md` avatar is sized to `CONTROL_CONTENT_HEIGHT` for
30
+ * exactly this. It takes the full `CONTROL_HEIGHT` band and no vertical
31
+ * bleed, so the token seats where every other control on the surface does
32
+ * instead of under-filling the band and standing a different height from the
33
+ * same chip un-peeked beside it.
34
+ *
35
+ * The HORIZONTAL bleed is common to both: the trigger's padding must not
36
+ * shift its content off the surface's left edge, or a peekable token sits 8px
37
+ * right of the plain one above it.
38
+ */
39
+ variant?: "text" | "control";
18
40
  side?: PopoverSide;
19
41
  align?: PopoverAlign;
20
42
  }
@@ -28,7 +50,7 @@ export interface PeekProps {
28
50
  * customer names, order ids, member names, linked records.
29
51
  */
30
52
  export function Peek(props: PeekProps) {
31
- const { children, content, accessibilityLabel, side = "bottom", align = "start" } = props;
53
+ const { children, content, accessibilityLabel, variant = "text", side = "bottom", align = "start" } = props;
32
54
  return (
33
55
  <Popover side={side} align={align}>
34
56
  <PopoverTrigger>
@@ -36,7 +58,7 @@ export function Peek(props: PeekProps) {
36
58
  focusRing
37
59
  accessibilityRole="button"
38
60
  accessibilityLabel={accessibilityLabel}
39
- style={styles.trigger}
61
+ style={[styles.trigger, variant === "control" ? styles.triggerControl : null]}
40
62
  // 32px visual, 40px touch target (32 + 2×4) — same as IconButton.
41
63
  hitSlop={4}
42
64
  >
@@ -51,9 +73,9 @@ export function Peek(props: PeekProps) {
51
73
  }
52
74
 
53
75
  const styles = StyleSheet.create({
54
- // Inline bleed at the 32px chip register (matches Badge scale, fits inside
55
- // text rows without inflating them); negative margins absorb the extra
56
- // height so the line's layout never shifts. hitSlop keeps the 40px target.
76
+ // The `text` register: a 32px chip box (Badge scale) whose extra height is
77
+ // bled back out with negative margins, so a reference inside a line of
78
+ // reading never grows that line. hitSlop keeps the 40px touch target.
57
79
  trigger: {
58
80
  flexDirection: "row",
59
81
  alignItems: "center",
@@ -63,6 +85,16 @@ const styles = StyleSheet.create({
63
85
  marginHorizontal: -8,
64
86
  marginVertical: -4,
65
87
  },
88
+ // The `control` register: the surface's own band. The vertical bleed goes —
89
+ // a control-band token is SUPPOSED to occupy the band — and the radius joins
90
+ // every other control's, so a peekable chip corners like the inputs it sits
91
+ // among rather than like a badge. The horizontal bleed stays, so the token
92
+ // keeps the left edge (see `variant`).
93
+ triggerControl: {
94
+ minHeight: CONTROL_HEIGHT,
95
+ borderRadius: CONTROL_RADIUS,
96
+ marginVertical: 0,
97
+ },
66
98
  content: {
67
99
  width: 320,
68
100
  },