@lotics/ui 14.2.0 → 14.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/docs/ai_patterns.md +26 -9
- package/docs/catalog.md +7 -4
- package/docs/templates.md +32 -20
- package/examples/tpl_item_list.tsx +319 -58
- package/examples/tpl_record.tsx +24 -15
- package/package.json +3 -6
- package/src/follow_scroll.tsx +32 -37
- package/examples/tpl_documents.tsx +0 -808
package/src/follow_scroll.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { FlatList, StyleProp, View, ViewStyle } from "react-native";
|
|
3
3
|
|
|
4
4
|
export interface FollowScrollProps {
|
|
5
5
|
children: ReactNode;
|
|
@@ -9,50 +9,45 @@ export interface FollowScrollProps {
|
|
|
9
9
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
// pinned, so sub-pixel scroll positions and streaming-growth rounding never
|
|
14
|
-
// silently unpin the follow.
|
|
15
|
-
const PIN_THRESHOLD = 40;
|
|
12
|
+
const DATA = ["content"];
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* A scroll container that FOLLOWS its growing content — pinned to the bottom, it
|
|
19
16
|
* keeps the newest content in view as children stream in (an `AgentRun` in a
|
|
20
|
-
* dialog, a log feed). Scrolling up
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* dialog, a log feed). Scrolling up releases the pin so the human can read
|
|
18
|
+
* history; scrolling back to the bottom re-pins.
|
|
19
|
+
*
|
|
20
|
+
* Implemented as a single-cell INVERTED list — the same mechanism as a chat
|
|
21
|
+
* message list — so the follow happens at LAYOUT level: the browser paints each
|
|
22
|
+
* growth already pinned (no scroll-after-paint flash, no scroll math). Short
|
|
23
|
+
* content still reads top-down (the content is end-justified in inverted
|
|
24
|
+
* coordinates); once it overflows, the newest edge stays in view natively. The
|
|
25
|
+
* inverted pattern's quirks (scrollbar thumb runs opposite, keyboard paging
|
|
26
|
+
* inverted) match the chat surface users already know.
|
|
24
27
|
*/
|
|
25
28
|
export function FollowScroll(props: FollowScrollProps) {
|
|
26
29
|
const { children, style, contentContainerStyle } = props;
|
|
27
|
-
const scrollRef = useRef<ScrollView>(null);
|
|
28
|
-
// Pinned state lives in a ref — it changes on every scroll tick and must never
|
|
29
|
-
// re-render the (streaming, already-busy) subtree.
|
|
30
|
-
const pinnedRef = useRef(true);
|
|
31
|
-
|
|
32
|
-
const followIfPinned = () => {
|
|
33
|
-
if (pinnedRef.current) scrollRef.current?.scrollToEnd({ animated: false });
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
37
|
-
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
38
|
-
const distanceFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
|
|
39
|
-
pinnedRef.current = distanceFromBottom < PIN_THRESHOLD;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
30
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
31
|
+
<FlatList
|
|
32
|
+
inverted
|
|
33
|
+
data={DATA}
|
|
34
|
+
keyExtractor={(k) => k}
|
|
35
|
+
// The consumer's content styles live on an inner wrapper INSIDE the cell —
|
|
36
|
+
// the cell renders upright (double flip), so authored padding keeps its
|
|
37
|
+
// visual orientation; the flipped outer container must not carry it.
|
|
38
|
+
renderItem={() => <View style={contentContainerStyle}>{children}</View>}
|
|
39
|
+
// Children identity changes every stream tick; without extraData the list
|
|
40
|
+
// would keep rendering the mounted cell's stale content.
|
|
41
|
+
extraData={children}
|
|
45
42
|
style={style}
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
onContentSizeChange={followIfPinned}
|
|
50
|
-
onLayout={followIfPinned}
|
|
51
|
-
onScroll={onScroll}
|
|
52
|
-
scrollEventThrottle={16}
|
|
43
|
+
// flexGrow + flex-end top-aligns SHORT content (flex-end is the visual TOP
|
|
44
|
+
// in inverted coordinates), so a feed reads top-down until it overflows.
|
|
45
|
+
contentContainerStyle={followStyles.grow}
|
|
53
46
|
nestedScrollEnabled
|
|
54
|
-
|
|
55
|
-
{children}
|
|
56
|
-
</ScrollView>
|
|
47
|
+
/>
|
|
57
48
|
);
|
|
58
49
|
}
|
|
50
|
+
|
|
51
|
+
const followStyles = {
|
|
52
|
+
grow: { flexGrow: 1, justifyContent: "flex-end" } as const,
|
|
53
|
+
};
|