@lotics/ui 43.2.0 → 43.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "43.2.0",
3
+ "version": "43.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -259,6 +259,11 @@ function fieldSurface(o: { variant: InlineEditVariant; disabled?: boolean; activ
259
259
  return (hovered: boolean): StyleProp<ViewStyle>[] => [
260
260
  styles.view,
261
261
  (o.variant === "bare" || o.disabled) && styles.viewBare,
262
+ // Keyed on the VARIANT alone, never on `disabled` — a disabled `framed`
263
+ // field shares the line above (it must not promise a press) but stays in its
264
+ // column with the enabled fields beside it. Bleeding that one would pull a
265
+ // read-only value 8px out of a form nobody could see was misaligned.
266
+ o.variant === "bare" && styles.viewBareBleed,
262
267
  CONTROL_TRANSITION,
263
268
  o.active && styles.viewActive,
264
269
  !o.active && hovered && !o.disabled && (o.variant === "bare" ? styles.viewHovered : styles.viewHoveredFramed),
@@ -557,7 +562,15 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
557
562
  }
558
563
 
559
564
  return (
560
- <View>
565
+ // THE SAME BLEED AS THE RESTING VIEW, on the element that is outermost HERE.
566
+ // A bare field must sit on its column in BOTH modes or it jumps 8px the
567
+ // moment you click it — which is how the half-done version of this read: the
568
+ // resting text aligned, and the input under the caret did not.
569
+ //
570
+ // Applied only when there are no verbs, because with them the shell below IS
571
+ // the outermost element and takes the bleed from `fieldSurface`. Applying it
572
+ // in both places would bleed 16.
573
+ <View style={actions == null && variant === "bare" ? styles.viewBareBleed : null}>
561
574
  <View style={styles.editRow}>
562
575
  {/* ONE SHELL, ACROSS BOTH MODES — not "the same shape rebuilt", the same
563
576
  ELEMENT. Only its contents swap.
@@ -673,6 +686,26 @@ const styles = StyleSheet.create({
673
686
  // Nothing at rest — the frame arrives on hover. Also how a DISABLED field
674
687
  // rests, whatever its variant: an inert value must not promise a press.
675
688
  viewBare: { backgroundColor: "transparent", borderColor: "transparent" },
689
+ // A bare field at rest looks like TEXT, so it aligns like text: its glyphs sit
690
+ // on the column, not its invisible box. The frame's 8px inset is real padding
691
+ // the eye cannot see at rest, and it put every bare value 8px right of the
692
+ // header naming it — measurably, in the grid AND on a record surface.
693
+ //
694
+ // The inset is pulled OUT rather than removed, because it is doing a second
695
+ // job: it is the air inside the border that fades in on hover. Setting it to 0
696
+ // would align the text and then draw the hover frame hard against the first
697
+ // glyph. Bleeding keeps both — text on the column, air inside the frame — and,
698
+ // critically, hover moves NOTHING, which zeroing padding at hover-time would
699
+ // not have managed.
700
+ //
701
+ // LEFT only. A matching right bleed looks symmetrical in the style sheet and is
702
+ // pure overhang on screen: it aligns nothing (the text is left-aligned) and
703
+ // pushes the frame 8px past the edge every neighbour stops at, which is
704
+ // visible the moment the border paints — a focused field ending 8px right of
705
+ // the row's own menu. With `marginLeft` alone the box grows leftward, its right
706
+ // edge stays on the container, and `paddingRight` still holds the text off the
707
+ // border.
708
+ viewBareBleed: { marginLeft: -8 },
676
709
  // The shell's own inset: the right edge tightens to 4 so an `InlineButton` sits
677
710
  // on the surface's edge with the same air a `Chip`'s dismiss gets.
678
711
  shell: { paddingRight: 4 },
@@ -23,7 +23,8 @@ export interface InlineTextInputProps {
23
23
  /**
24
24
  * Line budget for a value a reader has to read WHOLE rather than recognise —
25
25
  * a payment term, an address, a clause. Default 1 (the single-line field).
26
- * Above 1 the field is that many lines tall and wraps, in both states. Enter
26
+ * Above 1 the field is that many lines tall and wraps, in both states, and is
27
+ * what decides that Enter inserts a newline rather than committing. Enter
27
28
  * then inserts a newline; the field commits on blur (or the ✓ in "buttons").
28
29
  *
29
30
  * With `autoGrow` this becomes the MINIMUM rather than the whole budget.
@@ -78,16 +79,30 @@ export function InlineTextInput(props: InlineTextInputProps) {
78
79
  const { value, onSave, placeholder, controls = "blur", disabled, struck, accessibilityLabel , variant, actions, numberOfLines, autoGrow } = props;
79
80
  // Growing implies wrapping: a field that grows on one line has nowhere to go.
80
81
  const multiline = (numberOfLines ?? 1) > 1 || autoGrow === true;
82
+ // WHAT ENTER MEANS IS DECLARED BY `numberOfLines`, NOT BY WRAPPING. They were
83
+ // one flag, so `autoGrow` silently turned Enter into a newline — a keyboard
84
+ // change nobody can predict from a prop that says "fit the value".
85
+ //
86
+ // A field declared ONE line tall holds a single-line value that happens to
87
+ // wrap when it runs long (a title, a summary), and Enter is how you finish it.
88
+ // Declaring 2+ lines is declaring PROSE, where Enter is a paragraph break and
89
+ // blur commits. The two real callers split exactly there: a record entry's
90
+ // one-line gist (`numberOfLines={1} autoGrow`) against a three-line reserve.
91
+ const enterCommits = (numberOfLines ?? 1) <= 1;
81
92
  const edit = useInlineEdit<string>({ value, onSave });
82
93
 
83
94
  const onKeyPress = useCallback(
84
95
  (e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
85
96
  const key = e.nativeEvent.key;
86
97
  if (key === "Escape") edit.cancel();
87
- // On a multiline field Enter is a NEWLINE; the value commits on blur.
88
- else if (key === "Enter" && !multiline) void edit.commit();
98
+ else if (key === "Enter" && enterCommits) {
99
+ // On a WRAPPING field the browser would insert the newline as well, so
100
+ // committing without this leaves a stray line break in the saved value.
101
+ e.preventDefault();
102
+ void edit.commit();
103
+ }
89
104
  },
90
- [edit, multiline],
105
+ [edit, enterCommits],
91
106
  );
92
107
 
93
108
  // Commit on blur (the ergonomic default for moving through a form). In
package/src/timeline.tsx CHANGED
@@ -22,10 +22,15 @@ export interface TimelineItem {
22
22
  * for one entry, and the disc, which centres on the label row, ended up 80px
23
23
  * below the first line it was meant to sit beside.
24
24
  *
25
- * So a label that can run long belongs in `details` as well, in full. The
26
- * clamp is the contract rather than a prop: a prop would let the next caller
27
- * re-open the same hole, and the whole point is that the row keeps its beat
28
- * whatever the caller hands it.
25
+ * The clamp holds while the row is COLLAPSED and lifts when it opens, so a
26
+ * long label is readable in full without being copied anywhere. **Never repeat
27
+ * it in `details`** that was the old advice here, and it produced the same
28
+ * sentence twice, cut off above and whole below, on every caller that followed
29
+ * it. `details` carries what the label does not.
30
+ *
31
+ * The clamp is the contract rather than a prop: a prop would let the next
32
+ * caller re-open the same hole, and the whole point is that the row keeps its
33
+ * beat whatever the caller hands it.
29
34
  */
30
35
  label: string;
31
36
  /**
@@ -106,7 +111,13 @@ export function Timeline(props: TimelineProps) {
106
111
  <View style={styles.row}>
107
112
  <Text
108
113
  size="sm"
109
- numberOfLines={2}
114
+ // Clamped while COLLAPSED only. Expanding a row is a request to
115
+ // see more of what is there, and a label that stays truncated
116
+ // through it leaves the caller one way to make its own label
117
+ // readable: repeat it inside `details`. Then the reader meets the
118
+ // same sentence twice, once cut off and once whole, which reads as
119
+ // a rendering bug rather than as disclosure.
120
+ numberOfLines={expanded ? undefined : 2}
110
121
  // The same ink an unfilled field draws (`Not set`, `No date set`),
111
122
  // so an unwritten row reads as unwritten everywhere it appears.
112
123
  color={item.placeholder ? "zinc-400" : undefined}