@lotics/ui 43.2.0 → 43.3.1
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 +4 -9
- package/MIGRATION.md +44 -0
- package/docs/ai_patterns.md +11 -18
- package/docs/catalog.md +20 -28
- package/docs/composition.md +5 -16
- package/docs/data_entry.md +54 -38
- package/docs/templates.md +133 -48
- package/examples/tpl_record.tsx +349 -272
- package/package.json +1 -1
- package/src/inline_edit.tsx +34 -1
- package/src/inline_text_input.tsx +19 -4
- package/src/timeline.tsx +16 -5
package/package.json
CHANGED
package/src/inline_edit.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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,
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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}
|