@stream-mdx/react 0.3.0 → 0.5.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/CHANGELOG.md +14 -4
- package/README.md +65 -14
- package/dist/components/bottom-stick-scroll-area.cjs +344 -0
- package/dist/components/bottom-stick-scroll-area.d.cts +36 -0
- package/dist/components/bottom-stick-scroll-area.d.ts +36 -0
- package/dist/components/bottom-stick-scroll-area.mjs +320 -0
- package/dist/components/index.cjs +511 -176
- package/dist/components/index.d.cts +3 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.mjs +358 -26
- package/dist/{index-D7px9jug.d.cts → index-BVhp_pcF.d.ts} +4 -1
- package/dist/{index-D7px9jug.d.ts → index-EaeouYp2.d.cts} +4 -1
- package/dist/index.cjs +2146 -1145
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +3835 -2836
- package/dist/mdx-client.cjs +1 -1
- package/dist/mdx-client.mjs +1 -1
- package/dist/mdx-coordinator.cjs +28 -7
- package/dist/mdx-coordinator.d.cts +2 -2
- package/dist/mdx-coordinator.d.ts +2 -2
- package/dist/mdx-coordinator.mjs +28 -7
- package/dist/renderer/node-views.cjs +273 -24
- package/dist/renderer/node-views.d.cts +3 -1
- package/dist/renderer/node-views.d.ts +3 -1
- package/dist/renderer/node-views.mjs +274 -25
- package/dist/renderer/patch-batching.cjs +2 -0
- package/dist/renderer/patch-batching.d.cts +1 -1
- package/dist/renderer/patch-batching.d.ts +1 -1
- package/dist/renderer/patch-batching.mjs +2 -1
- package/dist/renderer/patch-commit-scheduler.cjs +82 -30
- package/dist/renderer/patch-commit-scheduler.d.cts +15 -3
- package/dist/renderer/patch-commit-scheduler.d.ts +15 -3
- package/dist/renderer/patch-commit-scheduler.mjs +84 -30
- package/dist/renderer/store.cjs +382 -36
- package/dist/renderer/store.d.cts +3 -0
- package/dist/renderer/store.d.ts +3 -0
- package/dist/renderer/store.mjs +382 -36
- package/dist/renderer.cjs +3386 -2727
- package/dist/renderer.d.cts +15 -12
- package/dist/renderer.d.ts +15 -12
- package/dist/renderer.mjs +3410 -2751
- package/dist/server.cjs +4842 -0
- package/dist/server.d.cts +254 -0
- package/dist/server.d.ts +254 -0
- package/dist/server.mjs +4824 -0
- package/dist/{streaming-markdown-Dp1IDgMT.d.ts → streaming-markdown-CEI9eDZm.d.ts} +3 -1
- package/dist/{streaming-markdown-DSC4L0xR.d.cts → streaming-markdown-DRisClXb.d.cts} +3 -1
- package/dist/streaming-markdown.cjs +3476 -2794
- package/dist/streaming-markdown.d.cts +4 -2
- package/dist/streaming-markdown.d.ts +4 -2
- package/dist/streaming-markdown.mjs +3436 -2754
- package/dist/utils/inline-html.d.cts +3 -1
- package/dist/utils/inline-html.d.ts +3 -1
- package/package.json +19 -3
- package/shadcn/README.md +28 -0
- package/shadcn/bottom-stick-scroll-area.tsx +358 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/components/bottom-stick-scroll-area.tsx
|
|
5
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var EPS_BOTTOM_PX = 2;
|
|
8
|
+
var EPS_OVERFLOW_PX = 1;
|
|
9
|
+
var DETACH_SCROLL_DELTA_PX = 1;
|
|
10
|
+
var RETURN_MIN_MS = 180;
|
|
11
|
+
var RETURN_MAX_MS = 520;
|
|
12
|
+
var RETURN_PX_PER_MS = 2.2;
|
|
13
|
+
var RETURN_SETTLE_FRAMES = 2;
|
|
14
|
+
function clamp01(value) {
|
|
15
|
+
if (value < 0) return 0;
|
|
16
|
+
if (value > 1) return 1;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function easeOutCubic(value) {
|
|
20
|
+
return 1 - (1 - value) ** 3;
|
|
21
|
+
}
|
|
22
|
+
function joinClasses(...parts) {
|
|
23
|
+
return parts.filter(Boolean).join(" ");
|
|
24
|
+
}
|
|
25
|
+
function measure(viewport) {
|
|
26
|
+
const scrollTop = viewport.scrollTop;
|
|
27
|
+
const scrollHeight = viewport.scrollHeight;
|
|
28
|
+
const clientHeight = viewport.clientHeight;
|
|
29
|
+
const maxScrollTop = Math.max(0, scrollHeight - clientHeight);
|
|
30
|
+
const distanceToBottom = Math.max(0, maxScrollTop - scrollTop);
|
|
31
|
+
const isOverflowing = scrollHeight > clientHeight + EPS_OVERFLOW_PX;
|
|
32
|
+
return { scrollTop, scrollHeight, clientHeight, maxScrollTop, distanceToBottom, isOverflowing };
|
|
33
|
+
}
|
|
34
|
+
function DefaultJumpButton({ canJump, jumpToBottom }) {
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
"button",
|
|
37
|
+
{
|
|
38
|
+
type: "button",
|
|
39
|
+
onClick: jumpToBottom,
|
|
40
|
+
"aria-label": "Scroll to bottom",
|
|
41
|
+
style: {
|
|
42
|
+
pointerEvents: canJump ? "auto" : "none",
|
|
43
|
+
borderRadius: 9999,
|
|
44
|
+
border: "1px solid color-mix(in srgb, currentColor 20%, transparent)",
|
|
45
|
+
width: 40,
|
|
46
|
+
height: 40,
|
|
47
|
+
display: "grid",
|
|
48
|
+
placeItems: "center",
|
|
49
|
+
boxShadow: "0 8px 20px rgba(0,0,0,0.18)",
|
|
50
|
+
background: "color-mix(in srgb, var(--background, #fff) 92%, transparent)",
|
|
51
|
+
color: "var(--foreground, #111)",
|
|
52
|
+
cursor: canJump ? "pointer" : "default"
|
|
53
|
+
},
|
|
54
|
+
children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2193" })
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function BottomStickScrollArea({
|
|
59
|
+
children,
|
|
60
|
+
className,
|
|
61
|
+
viewportClassName,
|
|
62
|
+
contentClassName,
|
|
63
|
+
showJumpToBottom = true,
|
|
64
|
+
onDebugStateChange,
|
|
65
|
+
onModeChange,
|
|
66
|
+
debugDomAttributes = false,
|
|
67
|
+
renderJumpToBottom,
|
|
68
|
+
jumpContainerClassName
|
|
69
|
+
}) {
|
|
70
|
+
const viewportRef = useRef(null);
|
|
71
|
+
const contentRef = useRef(null);
|
|
72
|
+
const modeRef = useRef("STICKY_INSTANT");
|
|
73
|
+
const [mode, setMode] = useState("STICKY_INSTANT");
|
|
74
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
75
|
+
const [distanceToBottom, setDistanceToBottom] = useState(0);
|
|
76
|
+
const prevScrollTopRef = useRef(0);
|
|
77
|
+
const lastProgrammaticWriteRef = useRef(null);
|
|
78
|
+
const programmaticWriteCountRef = useRef(0);
|
|
79
|
+
const pendingStickRafRef = useRef(null);
|
|
80
|
+
const stickRequestedRef = useRef(false);
|
|
81
|
+
const returnRafRef = useRef(null);
|
|
82
|
+
const returnAnimRef = useRef(null);
|
|
83
|
+
const setModeBoth = useCallback(
|
|
84
|
+
(next) => {
|
|
85
|
+
modeRef.current = next;
|
|
86
|
+
setMode(next);
|
|
87
|
+
onModeChange?.(next);
|
|
88
|
+
},
|
|
89
|
+
[onModeChange]
|
|
90
|
+
);
|
|
91
|
+
const updateDebug = useCallback(() => {
|
|
92
|
+
const viewport = viewportRef.current;
|
|
93
|
+
if (!viewport) return;
|
|
94
|
+
const m = measure(viewport);
|
|
95
|
+
if (debugDomAttributes) {
|
|
96
|
+
viewport.dataset.stickyMode = modeRef.current;
|
|
97
|
+
viewport.dataset.distanceToBottom = String(m.distanceToBottom);
|
|
98
|
+
viewport.dataset.isOverflowing = m.isOverflowing ? "1" : "0";
|
|
99
|
+
viewport.dataset.programmaticWrites = String(programmaticWriteCountRef.current);
|
|
100
|
+
}
|
|
101
|
+
onDebugStateChange?.({
|
|
102
|
+
mode: modeRef.current,
|
|
103
|
+
isOverflowing: m.isOverflowing,
|
|
104
|
+
distanceToBottom: m.distanceToBottom,
|
|
105
|
+
scrollTop: m.scrollTop,
|
|
106
|
+
maxScrollTop: m.maxScrollTop,
|
|
107
|
+
scrollHeight: m.scrollHeight,
|
|
108
|
+
clientHeight: m.clientHeight,
|
|
109
|
+
programmaticWrites: programmaticWriteCountRef.current
|
|
110
|
+
});
|
|
111
|
+
setDistanceToBottom(
|
|
112
|
+
(previous) => Math.abs(previous - m.distanceToBottom) <= 0.1 ? previous : m.distanceToBottom
|
|
113
|
+
);
|
|
114
|
+
}, [debugDomAttributes, onDebugStateChange]);
|
|
115
|
+
const writeScrollTop = useCallback((nextTop) => {
|
|
116
|
+
const viewport = viewportRef.current;
|
|
117
|
+
if (!viewport) return;
|
|
118
|
+
viewport.scrollTop = nextTop;
|
|
119
|
+
programmaticWriteCountRef.current += 1;
|
|
120
|
+
lastProgrammaticWriteRef.current = { at: performance.now(), top: nextTop };
|
|
121
|
+
}, []);
|
|
122
|
+
const cancelPendingStick = useCallback(() => {
|
|
123
|
+
if (pendingStickRafRef.current !== null) {
|
|
124
|
+
cancelAnimationFrame(pendingStickRafRef.current);
|
|
125
|
+
pendingStickRafRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
stickRequestedRef.current = false;
|
|
128
|
+
}, []);
|
|
129
|
+
const cancelReturn = useCallback(() => {
|
|
130
|
+
if (returnRafRef.current !== null) {
|
|
131
|
+
cancelAnimationFrame(returnRafRef.current);
|
|
132
|
+
returnRafRef.current = null;
|
|
133
|
+
}
|
|
134
|
+
returnAnimRef.current = null;
|
|
135
|
+
}, []);
|
|
136
|
+
const stickToBottomInstant = useCallback(() => {
|
|
137
|
+
const viewport = viewportRef.current;
|
|
138
|
+
if (!viewport) return;
|
|
139
|
+
stickRequestedRef.current = true;
|
|
140
|
+
if (pendingStickRafRef.current !== null) return;
|
|
141
|
+
pendingStickRafRef.current = requestAnimationFrame(() => {
|
|
142
|
+
pendingStickRafRef.current = null;
|
|
143
|
+
if (!stickRequestedRef.current) return;
|
|
144
|
+
stickRequestedRef.current = false;
|
|
145
|
+
if (modeRef.current !== "STICKY_INSTANT") return;
|
|
146
|
+
const m = measure(viewport);
|
|
147
|
+
writeScrollTop(m.maxScrollTop);
|
|
148
|
+
updateDebug();
|
|
149
|
+
});
|
|
150
|
+
}, [updateDebug, writeScrollTop]);
|
|
151
|
+
const startReturnToBottomSmooth = useCallback(() => {
|
|
152
|
+
const viewport = viewportRef.current;
|
|
153
|
+
if (!viewport) return;
|
|
154
|
+
cancelPendingStick();
|
|
155
|
+
cancelReturn();
|
|
156
|
+
const initial = measure(viewport);
|
|
157
|
+
if (initial.distanceToBottom <= EPS_BOTTOM_PX) {
|
|
158
|
+
setModeBoth("STICKY_INSTANT");
|
|
159
|
+
stickToBottomInstant();
|
|
160
|
+
updateDebug();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const durationMs = Math.max(
|
|
164
|
+
RETURN_MIN_MS,
|
|
165
|
+
Math.min(RETURN_MAX_MS, initial.distanceToBottom / RETURN_PX_PER_MS)
|
|
166
|
+
);
|
|
167
|
+
returnAnimRef.current = {
|
|
168
|
+
startTime: performance.now(),
|
|
169
|
+
startTop: initial.scrollTop,
|
|
170
|
+
durationMs,
|
|
171
|
+
settleFrames: 0
|
|
172
|
+
};
|
|
173
|
+
setModeBoth("RETURNING_SMOOTH");
|
|
174
|
+
const tick = (now) => {
|
|
175
|
+
const viewportCurrent = viewportRef.current;
|
|
176
|
+
const anim = returnAnimRef.current;
|
|
177
|
+
if (!viewportCurrent || !anim || modeRef.current !== "RETURNING_SMOOTH") {
|
|
178
|
+
cancelReturn();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const before = measure(viewportCurrent);
|
|
182
|
+
const progress = clamp01((now - anim.startTime) / anim.durationMs);
|
|
183
|
+
const eased = easeOutCubic(progress);
|
|
184
|
+
const targetBottom = before.maxScrollTop;
|
|
185
|
+
let nextTop = anim.startTop + (targetBottom - anim.startTop) * eased;
|
|
186
|
+
if (nextTop < before.scrollTop) {
|
|
187
|
+
nextTop = before.scrollTop;
|
|
188
|
+
}
|
|
189
|
+
writeScrollTop(nextTop);
|
|
190
|
+
const after = measure(viewportCurrent);
|
|
191
|
+
if (after.distanceToBottom <= EPS_BOTTOM_PX) {
|
|
192
|
+
anim.settleFrames += 1;
|
|
193
|
+
} else {
|
|
194
|
+
anim.settleFrames = 0;
|
|
195
|
+
}
|
|
196
|
+
const doneByTime = progress >= 1;
|
|
197
|
+
const doneBySettle = anim.settleFrames >= RETURN_SETTLE_FRAMES && progress >= 0.6;
|
|
198
|
+
if (doneByTime && doneBySettle) {
|
|
199
|
+
cancelReturn();
|
|
200
|
+
setModeBoth("STICKY_INSTANT");
|
|
201
|
+
stickToBottomInstant();
|
|
202
|
+
updateDebug();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
updateDebug();
|
|
206
|
+
returnRafRef.current = requestAnimationFrame(tick);
|
|
207
|
+
};
|
|
208
|
+
returnRafRef.current = requestAnimationFrame(tick);
|
|
209
|
+
}, [cancelPendingStick, cancelReturn, setModeBoth, stickToBottomInstant, updateDebug, writeScrollTop]);
|
|
210
|
+
const onViewportScroll = useCallback(
|
|
211
|
+
(event) => {
|
|
212
|
+
const viewport = event.currentTarget;
|
|
213
|
+
const m = measure(viewport);
|
|
214
|
+
setIsOverflowing((previous) => previous === m.isOverflowing ? previous : m.isOverflowing);
|
|
215
|
+
const previousTop = prevScrollTopRef.current;
|
|
216
|
+
const delta = m.scrollTop - previousTop;
|
|
217
|
+
const lastWrite = lastProgrammaticWriteRef.current;
|
|
218
|
+
const wroteRecently = lastWrite !== null && performance.now() - lastWrite.at < 60;
|
|
219
|
+
const isLikelyOwnWrite = wroteRecently && lastWrite !== null && Math.abs(m.scrollTop - lastWrite.top) <= 2;
|
|
220
|
+
if (modeRef.current === "RETURNING_SMOOTH") {
|
|
221
|
+
if (!isLikelyOwnWrite && delta < -DETACH_SCROLL_DELTA_PX) {
|
|
222
|
+
cancelReturn();
|
|
223
|
+
setModeBoth("DETACHED");
|
|
224
|
+
}
|
|
225
|
+
} else if (modeRef.current === "STICKY_INSTANT") {
|
|
226
|
+
if (!isLikelyOwnWrite && delta < -DETACH_SCROLL_DELTA_PX && m.isOverflowing) {
|
|
227
|
+
cancelPendingStick();
|
|
228
|
+
setModeBoth("DETACHED");
|
|
229
|
+
}
|
|
230
|
+
} else if (modeRef.current === "DETACHED") {
|
|
231
|
+
if (m.distanceToBottom <= EPS_BOTTOM_PX) {
|
|
232
|
+
setModeBoth("STICKY_INSTANT");
|
|
233
|
+
stickToBottomInstant();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
prevScrollTopRef.current = m.scrollTop;
|
|
237
|
+
updateDebug();
|
|
238
|
+
},
|
|
239
|
+
[cancelPendingStick, cancelReturn, setModeBoth, stickToBottomInstant, updateDebug]
|
|
240
|
+
);
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const viewport = viewportRef.current;
|
|
243
|
+
const content = contentRef.current;
|
|
244
|
+
if (!viewport || !content) return;
|
|
245
|
+
const observer = new ResizeObserver(() => {
|
|
246
|
+
const m = measure(viewport);
|
|
247
|
+
setIsOverflowing((previous) => previous === m.isOverflowing ? previous : m.isOverflowing);
|
|
248
|
+
if (modeRef.current === "STICKY_INSTANT") {
|
|
249
|
+
stickToBottomInstant();
|
|
250
|
+
}
|
|
251
|
+
updateDebug();
|
|
252
|
+
});
|
|
253
|
+
observer.observe(content);
|
|
254
|
+
observer.observe(viewport);
|
|
255
|
+
return () => observer.disconnect();
|
|
256
|
+
}, [stickToBottomInstant, updateDebug]);
|
|
257
|
+
useLayoutEffect(() => {
|
|
258
|
+
const viewport = viewportRef.current;
|
|
259
|
+
if (!viewport) return;
|
|
260
|
+
setModeBoth("STICKY_INSTANT");
|
|
261
|
+
const initial = measure(viewport);
|
|
262
|
+
writeScrollTop(initial.maxScrollTop);
|
|
263
|
+
prevScrollTopRef.current = viewport.scrollTop;
|
|
264
|
+
setIsOverflowing(initial.isOverflowing);
|
|
265
|
+
setDistanceToBottom(initial.distanceToBottom);
|
|
266
|
+
updateDebug();
|
|
267
|
+
}, [setModeBoth, updateDebug, writeScrollTop]);
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
return () => {
|
|
270
|
+
cancelPendingStick();
|
|
271
|
+
cancelReturn();
|
|
272
|
+
};
|
|
273
|
+
}, [cancelPendingStick, cancelReturn]);
|
|
274
|
+
const canJump = isOverflowing && mode === "DETACHED";
|
|
275
|
+
const jumpButton = renderJumpToBottom?.({
|
|
276
|
+
mode,
|
|
277
|
+
canJump,
|
|
278
|
+
isOverflowing,
|
|
279
|
+
distanceToBottom,
|
|
280
|
+
jumpToBottom: startReturnToBottomSmooth
|
|
281
|
+
}) ?? /* @__PURE__ */ jsx(DefaultJumpButton, { canJump, jumpToBottom: startReturnToBottomSmooth });
|
|
282
|
+
const jumpContainerClassNameBase = "pointer-events-none absolute z-20";
|
|
283
|
+
const jumpContainerStyle = {
|
|
284
|
+
position: "absolute",
|
|
285
|
+
zIndex: 20,
|
|
286
|
+
left: 0,
|
|
287
|
+
right: 0,
|
|
288
|
+
bottom: 12,
|
|
289
|
+
display: "flex",
|
|
290
|
+
justifyContent: "center",
|
|
291
|
+
opacity: canJump ? 1 : 0,
|
|
292
|
+
transform: canJump ? "translateY(0px)" : "translateY(4px)",
|
|
293
|
+
transition: "opacity 150ms ease, transform 150ms ease",
|
|
294
|
+
pointerEvents: "none"
|
|
295
|
+
};
|
|
296
|
+
return /* @__PURE__ */ jsxs("div", { className: joinClasses("relative h-full w-full overflow-hidden", className), children: [
|
|
297
|
+
/* @__PURE__ */ jsx(
|
|
298
|
+
"div",
|
|
299
|
+
{
|
|
300
|
+
ref: viewportRef,
|
|
301
|
+
className: joinClasses("h-full w-full overflow-auto", viewportClassName),
|
|
302
|
+
onScroll: onViewportScroll,
|
|
303
|
+
"data-testid": "sticky-scroll-viewport",
|
|
304
|
+
children: /* @__PURE__ */ jsx("div", { ref: contentRef, className: joinClasses("flex min-h-full w-full flex-col", contentClassName), children })
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
showJumpToBottom ? /* @__PURE__ */ jsx(
|
|
308
|
+
"div",
|
|
309
|
+
{
|
|
310
|
+
className: joinClasses(jumpContainerClassNameBase, jumpContainerClassName),
|
|
311
|
+
style: jumpContainerStyle,
|
|
312
|
+
"aria-hidden": !canJump,
|
|
313
|
+
children: /* @__PURE__ */ jsx("div", { className: canJump ? "pointer-events-auto" : "pointer-events-none", children: jumpButton })
|
|
314
|
+
}
|
|
315
|
+
) : null
|
|
316
|
+
] });
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
BottomStickScrollArea
|
|
320
|
+
};
|