@stream-mdx/react 0.4.0 → 1.0.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +385 -4
  2. package/README.md +141 -16
  3. package/dist/blocks-renderer-DAOBFBfi.d.ts +29 -0
  4. package/dist/blocks-renderer-DQEL1kJW.d.cts +29 -0
  5. package/dist/components/bottom-stick-scroll-area.cjs +268 -0
  6. package/dist/components/bottom-stick-scroll-area.d.cts +36 -0
  7. package/dist/components/bottom-stick-scroll-area.d.ts +36 -0
  8. package/dist/components/bottom-stick-scroll-area.mjs +249 -0
  9. package/dist/components/diff-renderer.cjs +86 -0
  10. package/dist/components/diff-renderer.d.cts +11 -0
  11. package/dist/components/diff-renderer.d.ts +11 -0
  12. package/dist/components/diff-renderer.mjs +66 -0
  13. package/dist/components/index.cjs +2879 -1295
  14. package/dist/components/index.d.cts +9 -2
  15. package/dist/components/index.d.ts +9 -2
  16. package/dist/components/index.mjs +2842 -1277
  17. package/dist/components/katex-math.cjs +220 -0
  18. package/dist/components/katex-math.d.cts +10 -0
  19. package/dist/components/katex-math.d.ts +10 -0
  20. package/dist/components/katex-math.mjs +188 -0
  21. package/dist/contexts/math-tracker.cjs +240 -521
  22. package/dist/contexts/math-tracker.mjs +235 -513
  23. package/dist/index-Cmne1yUK.d.cts +723 -0
  24. package/dist/index-D7dyyu3q.d.ts +723 -0
  25. package/dist/index.cjs +13180 -5640
  26. package/dist/index.d.cts +72 -40
  27. package/dist/index.d.ts +72 -40
  28. package/dist/index.mjs +12587 -5038
  29. package/dist/math/display-wrapper.cjs +7 -15
  30. package/dist/math/display-wrapper.d.cts +2 -2
  31. package/dist/math/display-wrapper.d.ts +2 -2
  32. package/dist/math/display-wrapper.mjs +2 -6
  33. package/dist/node-views-Bpc6Gkgl.d.cts +13 -0
  34. package/dist/node-views-DpnYdaAS.d.ts +13 -0
  35. package/dist/renderer/hooks.cjs +35 -20
  36. package/dist/renderer/hooks.d.cts +22 -1
  37. package/dist/renderer/hooks.d.ts +22 -1
  38. package/dist/renderer/hooks.mjs +27 -12
  39. package/dist/renderer/patch-batching.cjs +7 -8
  40. package/dist/renderer/patch-batching.d.cts +1 -1
  41. package/dist/renderer/patch-batching.d.ts +1 -1
  42. package/dist/renderer/patch-batching.mjs +2 -1
  43. package/dist/renderer/patch-coalescing.cjs +5 -8
  44. package/dist/renderer/patch-commit-scheduler.cjs +160 -326
  45. package/dist/renderer/patch-commit-scheduler.d.cts +28 -4
  46. package/dist/renderer/patch-commit-scheduler.d.ts +28 -4
  47. package/dist/renderer/patch-commit-scheduler.mjs +157 -318
  48. package/dist/renderer/store.cjs +3847 -1099
  49. package/dist/renderer/store.d.cts +111 -3
  50. package/dist/renderer/store.d.ts +111 -3
  51. package/dist/renderer/store.mjs +3847 -1085
  52. package/dist/renderer/virtualized-code.cjs +415 -71
  53. package/dist/renderer/virtualized-code.d.cts +24 -23
  54. package/dist/renderer/virtualized-code.d.ts +24 -23
  55. package/dist/renderer/virtualized-code.mjs +409 -62
  56. package/dist/renderer.cjs +10939 -3974
  57. package/dist/renderer.d.cts +100 -29
  58. package/dist/renderer.d.ts +100 -29
  59. package/dist/renderer.mjs +10711 -3697
  60. package/dist/server-regions-CPLHGli0.d.cts +32 -0
  61. package/dist/server-regions-mUhyoKi7.d.ts +32 -0
  62. package/dist/server.cjs +10286 -3365
  63. package/dist/server.d.cts +17 -247
  64. package/dist/server.d.ts +17 -247
  65. package/dist/server.mjs +10565 -3619
  66. package/dist/{streaming-markdown-DOc2uLiP.d.ts → streaming-markdown-yELbJxyH.d.ts} +51 -55
  67. package/dist/{streaming-markdown-D-JcByd9.d.cts → streaming-markdown-zVwFuaJG.d.cts} +51 -55
  68. package/dist/streaming-markdown.cjs +11828 -4879
  69. package/dist/streaming-markdown.d.cts +9 -2
  70. package/dist/streaming-markdown.d.ts +9 -2
  71. package/dist/streaming-markdown.mjs +11820 -4821
  72. package/dist/trusted-mdx/index.cjs +833 -0
  73. package/dist/trusted-mdx/index.d.cts +11 -0
  74. package/dist/trusted-mdx/index.d.ts +11 -0
  75. package/dist/trusted-mdx/index.mjs +807 -0
  76. package/dist/trusted-mdx/native-children-slot.cjs +45 -0
  77. package/dist/trusted-mdx/native-children-slot.d.cts +11 -0
  78. package/dist/trusted-mdx/native-children-slot.d.ts +11 -0
  79. package/dist/trusted-mdx/native-children-slot.mjs +24 -0
  80. package/dist/trusted-mdx/server.cjs +752 -0
  81. package/dist/trusted-mdx/server.d.cts +24 -0
  82. package/dist/trusted-mdx/server.d.ts +24 -0
  83. package/dist/trusted-mdx/server.mjs +724 -0
  84. package/dist/trusted-mdx-contract-ElKpkDI5.d.cts +113 -0
  85. package/dist/trusted-mdx-contract-ElKpkDI5.d.ts +113 -0
  86. package/dist/utils/inline-html.cjs +246 -147
  87. package/dist/utils/inline-html.d.cts +39 -6
  88. package/dist/utils/inline-html.d.ts +39 -6
  89. package/dist/utils/inline-html.mjs +237 -134
  90. package/package.json +52 -18
  91. package/shadcn/README.md +28 -0
  92. package/shadcn/bottom-stick-scroll-area.tsx +358 -0
  93. package/dist/index-D2MYusAM.d.cts +0 -248
  94. package/dist/index-D2MYusAM.d.ts +0 -248
  95. package/dist/mdx-client.cjs +0 -408
  96. package/dist/mdx-client.d.cts +0 -113
  97. package/dist/mdx-client.d.ts +0 -113
  98. package/dist/mdx-client.mjs +0 -369
  99. package/dist/mdx-coordinator.cjs +0 -563
  100. package/dist/mdx-coordinator.d.cts +0 -21
  101. package/dist/mdx-coordinator.d.ts +0 -21
  102. package/dist/mdx-coordinator.mjs +0 -529
  103. package/dist/renderer/node-views.cjs +0 -1642
  104. package/dist/renderer/node-views.d.cts +0 -12
  105. package/dist/renderer/node-views.d.ts +0 -12
  106. package/dist/renderer/node-views.mjs +0 -1616
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # `@stream-mdx/react`
2
2
 
3
- React renderer for StreamMDX. Exposes the `<StreamingMarkdown />` component, scheduling/backpressure hooks, and the public types used by consumers.
3
+ `@stream-mdx/react` is the renderer package for StreamMDX. It owns the React component surface, the renderer store, patch scheduling, server rendering helpers, and the packaged bottom-stick scroll component used by the docs/demo surfaces.
4
4
 
5
- Most apps should install `stream-mdx` instead of this package directly. Use `@stream-mdx/react` when you want the React surface without the unscoped wrapper.
5
+ **Most apps should install [`stream-mdx`](../stream-mdx/README.md) instead.** Use this package directly when you want the React layer without the convenience wrapper.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,19 +10,33 @@ Most apps should install `stream-mdx` instead of this package directly. Use `@st
10
10
  npm install @stream-mdx/react @stream-mdx/worker
11
11
  ```
12
12
 
13
- `react` and `react-dom` are peer dependencies.
13
+ Peer dependencies:
14
14
 
15
- ## Quickstart
15
+ | Package | Range |
16
+ | --- | --- |
17
+ | `react` | `>=18.2.0` |
18
+ | `react-dom` | `>=18.2.0` |
19
+
20
+ ## Primary Exports
16
21
 
17
- Copy the hosted worker bundle into your app’s static assets:
22
+ | Export | Purpose |
23
+ | --- | --- |
24
+ | `@stream-mdx/react` | Main React surface including `<StreamingMarkdown />` |
25
+ | `@stream-mdx/react/server` | Server/static render helpers |
26
+ | `@stream-mdx/react/components` | Shared UI component exports |
27
+ | `@stream-mdx/react/components/bottom-stick-scroll-area` | Packaged sticky-bottom scroll container |
28
+ | `@stream-mdx/react/renderer` | Lower-level renderer exports |
29
+ | `@stream-mdx/react/renderer/patch-commit-scheduler` | Scheduler implementation surface |
30
+ | `@stream-mdx/react/renderer/store` | Renderer store surface |
31
+ | `@stream-mdx/react/trusted-mdx` | Explicit trusted-MDX engine factory and pure contract types |
32
+
33
+ ## Quickstart
18
34
 
19
35
  ```bash
20
36
  mkdir -p public/workers
21
37
  cp node_modules/@stream-mdx/worker/dist/hosted/markdown-worker.js public/workers/markdown-worker.js
22
38
  ```
23
39
 
24
- Render streaming markdown from a client component:
25
-
26
40
  ```tsx
27
41
  "use client";
28
42
 
@@ -33,20 +47,131 @@ export function Demo({ text }: { text: string }) {
33
47
  <StreamingMarkdown
34
48
  text={text}
35
49
  worker="/workers/markdown-worker.js"
36
- features={{ html: true, tables: true, math: true, mdx: true }}
37
- mdxCompileMode="worker"
50
+ features={{ html: true, tables: true, math: true, footnotes: true }}
38
51
  />
39
52
  );
40
53
  }
41
54
  ```
42
55
 
43
- ## Docs
56
+ `engine` defaults to `"auto"` and bootstraps up to 4,096 accepted UTF-16 source units with the existing inline engine while worker setup and grammar prewarm are pending. Further source is deferred beyond that limit. `"worker"` requires a Worker; `"main"` runs parsing on the browser thread without a bootstrap cap. Engine setup runs after mounting, not during SSR. Use compiled snapshots and the server entry for server markup.
57
+
58
+ ### Rendering customization
59
+
60
+ `components` replaces the removed renderer maps:
61
+
62
+ - `inlineComponents` → `components["inline:<kind>"]` or `components["extension:<name>"]`
63
+ - `htmlElements` → `components["html:<tag>"]`
64
+ - `tableElements` → `components.table` wrappers or headless table views with `useTableBlock` / `useTableRows`
65
+ - `codeRenderers` → `components.code` wrappers or headless code views; wrappers receive `metadata.language`, and `slots.actions` replaces native code actions. Mermaid diagrams use `engines.mermaid` from `createMermaidEngine()`
66
+ - `mathRenderer` → presentation becomes a math wrapper/view in `components["inline:math"]`; TeX rendering uses `engines.math` (KaTeX remains the default)
67
+ - `mdxComponents` → restricted `contentComponents` registrations, or trusted `contentPolicy="trusted-mdx"` with `engines.mdx`
68
+
69
+ `features.math: false` disables math parsing. See the [public API](../../docs/PUBLIC_API.md) for HTML policy, restricted/trusted MDX, inert artifact fences, and optional finalized-block unified routing.
70
+
71
+ `colorScheme` accepts `"light"`, `"dark"`, or `"system"` and sets the native root's `data-color-scheme`. System mode follows `prefers-color-scheme` with CSS only; when unset, a `.dark` ancestor keeps the existing class-based behavior. Native roots carry `data-stream-mdx` and `not-prose`, so enclosing typography-plugin selectors do not restyle native output and the optional Tailwind theme scopes native styles to that root.
72
+
73
+ ## Typical Uses
74
+
75
+ | Use case | API |
76
+ | --- | --- |
77
+ | Browser/client rendering | `<StreamingMarkdown />` |
78
+ | SSR / SSG / static export rendering | `MarkdownBlocksRenderer` from `@stream-mdx/react/server` |
79
+ | Rich streaming/chat container | `BottomStickScrollArea` |
80
+ | Advanced renderer internals | `renderer/store`, `renderer/patch-commit-scheduler` |
81
+
82
+ ### Server-side block rendering
83
+
84
+ ```tsx
85
+ import { ComponentRegistry, MarkdownBlocksRenderer } from "@stream-mdx/react/server";
86
+
87
+ return <MarkdownBlocksRenderer blocks={blocks} componentRegistry={new ComponentRegistry()} />;
88
+ ```
89
+
90
+ Default math stays as authored TeX source on the server and on the client's first render. KaTeX loads after commit and replaces that source after mount, so cold and preloaded clients hydrate from the same markup.
91
+
92
+ ### Bottom-stick scroll area
93
+
94
+ ```tsx
95
+ import { BottomStickScrollArea } from "@stream-mdx/react/components/bottom-stick-scroll-area";
96
+
97
+ <BottomStickScrollArea className="h-[32rem]">{children}</BottomStickScrollArea>;
98
+ ```
99
+
100
+ Behavior notes:
101
+
102
+ - sticks to bottom while content appends
103
+ - detaches on upward user scroll
104
+ - supports smooth return-to-bottom behavior
105
+ - honors `prefers-reduced-motion: reduce` with instant jumps and no jump-chrome transition
106
+ - interrupts an active smooth return on wheel, touch, or scroll-key input without preventing native behavior
107
+ - exposes debug hooks used by deterministic checks in the repo
108
+
109
+ ### Table row virtualization
110
+
111
+ ```tsx
112
+ import { StreamingMarkdown } from "@stream-mdx/react";
113
+
114
+ <StreamingMarkdown
115
+ text={content}
116
+ tableVirtualization={{
117
+ enabled: true,
118
+ minRows: 40,
119
+ estimatedRowHeight: 44,
120
+ overscan: 8,
121
+ maxHeight: "600px",
122
+ }}
123
+ />
124
+ ```
125
+
126
+ Table virtualization is disabled by default. Only finalized tables meeting `minRows` virtualize after mounting in a browser and after the surrounding stream ends. SSR, unfinished tables, and active streams render every row.
127
+
128
+ Virtualized tables retain native table elements, a sticky header, measured variable row heights, and `aria-hidden` spacer rows. The last row remains reachable by scrolling.
129
+ When detached from the bottom, row measurements preserve the visible row and its pixel offset, including content partly covered by the sticky header. Scrolling chooses a new anchor; bottom attachment still follows measured tail growth.
130
+
131
+ ### Code viewport and printing
132
+
133
+ Large native code blocks use a bounded reader viewport. The **Show full content** control mounts all authored lines while retaining the existing code wrapper and mounted native rows; toggling back restores the bounded window and reader anchor.
134
+
135
+ `StreamingMarkdown` accepts `codeVirtualization?: Partial<VirtualizedCodeConfig>` to configure native code viewport presentation:
136
+
137
+ ```tsx
138
+ <StreamingMarkdown text={content} codeVirtualization={{ followTail: true }} />
139
+ ```
140
+
141
+ `followTail` defaults to `false`, preserving the reader position when code is appended. Set it to `true` to opt in to following appended rows while attached at the physical bottom (within 2px). An upward scroll of more than 1px detaches; returning within 2px reattaches. Changing this presentation config does not reset authored source or replace the native viewport; turning `followTail` off preserves the existing reader anchor and scroll position on a subsequent append.
142
+
143
+ When a browser dispatches `beforeprint`, an eligible virtualized native code block temporarily mounts its full source and removes viewport clipping and height limits. `afterprint` restores the prior reader mode, scroll anchor, and native text selection without changing the manual full-content control.
144
+
145
+ Print preparation mounts the whole block in the DOM, so print-time memory and layout work scale with the source length. Custom code renderers and server-rendered output control their own print behavior.
146
+
147
+ Native whole-code copy/download uses cold authored source, preserving CR/LF and trailing blank lines rather than normalized display text. The test DOM exercises copy with an active selection and a Clipboard API stub; no physical browser or OS clipboard proof is claimed. Download shares the source action, but direct Blob browser proof remains pending. Native copied-status feedback cleanup prevents delayed updates after unmount.
148
+ Each eligible virtualized table also exposes a "Show full content" toggle. It mounts every row only on request; toggling it again restores the bounded viewport without replacing the native table or changing the reader's scroll position.
149
+
150
+
151
+ ### Table controls
152
+
153
+ Table controls are off by default. Enable Markdown copy or download for tables with `controls`:
154
+
155
+ ```tsx
156
+ <StreamingMarkdown
157
+ text={content}
158
+ features={{ tables: true }}
159
+ controls={{ table: { copy: ["markdown"], download: ["markdown"] } }}
160
+ />
161
+ ```
162
+
163
+ For native tables, Markdown copy and download use the table's authored source slice. They preserve spacing, escaped pipes, inline Markdown, and original line endings, and exclude the final row's line ending and any following document content. TSV and CSV remain cell-value transforms, not source-preserving Markdown.
164
+
165
+ ## When To Use This Package Directly
44
166
 
45
- - API reference: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/PUBLIC_API.md
46
- - React integration guide: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/REACT_INTEGRATION_GUIDE.md
47
- - Plugins & worker customization: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/STREAMING_MARKDOWN_PLUGINS_COOKBOOK.md
48
- - Full docs site: https://kmccleary3301.github.io/stream-mdx/
167
+ - You are shipping a library that wants `@stream-mdx/react` explicitly in `peerDependencies` or `dependencies`.
168
+ - You need server rendering helpers without the convenience wrapper import path.
169
+ - You are working on renderer internals and want the lower-level exports directly.
49
170
 
50
- ## Addons
171
+ ## Documentation
51
172
 
52
- - `@stream-mdx/mermaid` (optional Mermaid diagram renderer)
173
+ - [`../../docs/PUBLIC_API.md`](../../docs/PUBLIC_API.md)
174
+ - [`../../docs/REACT_INTEGRATION_GUIDE.md`](../../docs/REACT_INTEGRATION_GUIDE.md)
175
+ - [`../../docs/SECURITY_MODEL.md`](../../docs/SECURITY_MODEL.md)
176
+ - [`../../docs/STREAMING_CORRECTNESS_CONTRACT.md`](../../docs/STREAMING_CORRECTNESS_CONTRACT.md)
177
+ - Docs site: <https://stream-mdx.vercel.app/docs>
@@ -0,0 +1,29 @@
1
+ import { Block, UrlTransform } from '@stream-mdx/core';
2
+ import React__default from 'react';
3
+ import { l as ElementFilterOptions, d as ComponentRegistry, _ as NodePresentations, b as ControlsConfig, c as ControlsComponentProps, w as AnimatedProp, C as ContentComponentsMap, $ as MermaidEngine } from './index-D7dyyu3q.js';
4
+ import { RendererStore } from './renderer/store.js';
5
+
6
+ /**
7
+ * React component for rendering markdown blocks
8
+ */
9
+ interface MarkdownBlocksRendererProps extends ElementFilterOptions {
10
+ blocks: ReadonlyArray<Block>;
11
+ componentRegistry?: ComponentRegistry;
12
+ className?: string;
13
+ style?: React__default.CSSProperties;
14
+ colorScheme?: "light" | "dark" | "system";
15
+ store?: RendererStore;
16
+ components?: NodePresentations;
17
+ urlTransform?: UrlTransform;
18
+ controls?: ControlsConfig;
19
+ controlsComponent?: React__default.ComponentType<ControlsComponentProps>;
20
+ animated?: AnimatedProp;
21
+ statusMessage?: string;
22
+ contentComponents?: ContentComponentsMap;
23
+ engines?: {
24
+ mermaid?: MermaidEngine;
25
+ };
26
+ }
27
+ declare const MarkdownBlocksRenderer: React__default.NamedExoticComponent<MarkdownBlocksRendererProps>;
28
+
29
+ export { MarkdownBlocksRenderer as M, type MarkdownBlocksRendererProps as a };
@@ -0,0 +1,29 @@
1
+ import { Block, UrlTransform } from '@stream-mdx/core';
2
+ import React__default from 'react';
3
+ import { l as ElementFilterOptions, d as ComponentRegistry, _ as NodePresentations, b as ControlsConfig, c as ControlsComponentProps, w as AnimatedProp, C as ContentComponentsMap, $ as MermaidEngine } from './index-Cmne1yUK.cjs';
4
+ import { RendererStore } from './renderer/store.cjs';
5
+
6
+ /**
7
+ * React component for rendering markdown blocks
8
+ */
9
+ interface MarkdownBlocksRendererProps extends ElementFilterOptions {
10
+ blocks: ReadonlyArray<Block>;
11
+ componentRegistry?: ComponentRegistry;
12
+ className?: string;
13
+ style?: React__default.CSSProperties;
14
+ colorScheme?: "light" | "dark" | "system";
15
+ store?: RendererStore;
16
+ components?: NodePresentations;
17
+ urlTransform?: UrlTransform;
18
+ controls?: ControlsConfig;
19
+ controlsComponent?: React__default.ComponentType<ControlsComponentProps>;
20
+ animated?: AnimatedProp;
21
+ statusMessage?: string;
22
+ contentComponents?: ContentComponentsMap;
23
+ engines?: {
24
+ mermaid?: MermaidEngine;
25
+ };
26
+ }
27
+ declare const MarkdownBlocksRenderer: React__default.NamedExoticComponent<MarkdownBlocksRendererProps>;
28
+
29
+ export { MarkdownBlocksRenderer as M, type MarkdownBlocksRendererProps as a };
@@ -0,0 +1,268 @@
1
+ "use client";
2
+ "use strict";
3
+ "use client";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: !0 });
11
+ }, __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from == "object" || typeof from == "function")
13
+ for (let key of __getOwnPropNames(from))
14
+ !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: !0 }), mod);
18
+
19
+ // src/components/bottom-stick-scroll-area.tsx
20
+ var bottom_stick_scroll_area_exports = {};
21
+ __export(bottom_stick_scroll_area_exports, {
22
+ BottomStickScrollArea: () => BottomStickScrollArea
23
+ });
24
+ module.exports = __toCommonJS(bottom_stick_scroll_area_exports);
25
+ var import_react = require("react"), import_jsx_runtime = require("react/jsx-runtime"), EPS_BOTTOM_PX = 2, EPS_OVERFLOW_PX = 1, DETACH_SCROLL_DELTA_PX = 1, RETURN_MIN_MS = 180, RETURN_MAX_MS = 520, RETURN_PX_PER_MS = 2.2, RETURN_SETTLE_FRAMES = 2;
26
+ function clamp01(value) {
27
+ return value < 0 ? 0 : value > 1 ? 1 : value;
28
+ }
29
+ function easeOutCubic(value) {
30
+ return 1 - (1 - value) ** 3;
31
+ }
32
+ function joinClasses(...parts) {
33
+ return parts.filter(Boolean).join(" ");
34
+ }
35
+ function measure(viewport) {
36
+ let scrollTop = viewport.scrollTop, scrollHeight = viewport.scrollHeight, clientHeight = viewport.clientHeight, maxScrollTop = Math.max(0, scrollHeight - clientHeight), distanceToBottom = Math.max(0, maxScrollTop - scrollTop), isOverflowing = scrollHeight > clientHeight + EPS_OVERFLOW_PX;
37
+ return { scrollTop, scrollHeight, clientHeight, maxScrollTop, distanceToBottom, isOverflowing };
38
+ }
39
+ function isEditableTarget(target) {
40
+ if (!(target instanceof Element)) return !1;
41
+ if (target.closest("input, textarea, select, [role='textbox']")) return !0;
42
+ let contentEditable = target.closest("[contenteditable]");
43
+ return contentEditable !== null && contentEditable.getAttribute("contenteditable")?.toLowerCase() !== "false";
44
+ }
45
+ function isScrollKeyIntent(event) {
46
+ if (event.defaultPrevented || isEditableTarget(event.target)) return !1;
47
+ switch (event.key) {
48
+ case "ArrowDown":
49
+ case "ArrowUp":
50
+ case "PageDown":
51
+ case "PageUp":
52
+ case "Home":
53
+ case "End":
54
+ return !0;
55
+ case " ":
56
+ case "Spacebar":
57
+ return event.target instanceof Element ? event.target.closest(
58
+ "button, input, summary, [role='button'], [role='checkbox'], [role='switch'], [role='radio'], [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']"
59
+ ) === null : !0;
60
+ default:
61
+ return !1;
62
+ }
63
+ }
64
+ function DefaultJumpButton({ canJump, jumpToBottom }) {
65
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
66
+ "button",
67
+ {
68
+ type: "button",
69
+ onClick: jumpToBottom,
70
+ "aria-label": "Scroll to bottom",
71
+ style: {
72
+ pointerEvents: canJump ? "auto" : "none",
73
+ borderRadius: 9999,
74
+ border: "1px solid color-mix(in srgb, currentColor 20%, transparent)",
75
+ width: 40,
76
+ height: 40,
77
+ display: "grid",
78
+ placeItems: "center",
79
+ boxShadow: "0 8px 20px rgba(0,0,0,0.18)",
80
+ background: "color-mix(in srgb, var(--background, #fff) 92%, transparent)",
81
+ color: "var(--foreground, #111)",
82
+ cursor: canJump ? "pointer" : "default"
83
+ },
84
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": !0, children: "\u2193" })
85
+ }
86
+ );
87
+ }
88
+ function BottomStickScrollArea({
89
+ children,
90
+ className,
91
+ viewportClassName,
92
+ contentClassName,
93
+ showJumpToBottom = !0,
94
+ onDebugStateChange,
95
+ onModeChange,
96
+ debugDomAttributes = !1,
97
+ renderJumpToBottom,
98
+ jumpContainerClassName
99
+ }) {
100
+ let viewportRef = (0, import_react.useRef)(null), contentRef = (0, import_react.useRef)(null), modeRef = (0, import_react.useRef)("STICKY_INSTANT"), [mode, setMode] = (0, import_react.useState)("STICKY_INSTANT"), [isOverflowing, setIsOverflowing] = (0, import_react.useState)(!1), [distanceToBottom, setDistanceToBottom] = (0, import_react.useState)(0), [prefersReducedMotion, setPrefersReducedMotion] = (0, import_react.useState)(!1), prefersReducedMotionRef = (0, import_react.useRef)(!1), prevScrollTopRef = (0, import_react.useRef)(0), lastProgrammaticWriteRef = (0, import_react.useRef)(null), programmaticWriteCountRef = (0, import_react.useRef)(0), pendingStickRafRef = (0, import_react.useRef)(null), stickRequestedRef = (0, import_react.useRef)(!1), returnRafRef = (0, import_react.useRef)(null), returnAnimRef = (0, import_react.useRef)(null), setModeBoth = (0, import_react.useCallback)(
101
+ (next) => {
102
+ modeRef.current = next, setMode(next), onModeChange?.(next);
103
+ },
104
+ [onModeChange]
105
+ ), updateDebug = (0, import_react.useCallback)(() => {
106
+ let viewport = viewportRef.current;
107
+ if (!viewport) return;
108
+ let m = measure(viewport);
109
+ debugDomAttributes && (viewport.dataset.stickyMode = modeRef.current, viewport.dataset.distanceToBottom = String(m.distanceToBottom), viewport.dataset.isOverflowing = m.isOverflowing ? "1" : "0", viewport.dataset.programmaticWrites = String(programmaticWriteCountRef.current)), onDebugStateChange?.({
110
+ mode: modeRef.current,
111
+ isOverflowing: m.isOverflowing,
112
+ distanceToBottom: m.distanceToBottom,
113
+ scrollTop: m.scrollTop,
114
+ maxScrollTop: m.maxScrollTop,
115
+ scrollHeight: m.scrollHeight,
116
+ clientHeight: m.clientHeight,
117
+ programmaticWrites: programmaticWriteCountRef.current
118
+ }), setDistanceToBottom(
119
+ (previous) => Math.abs(previous - m.distanceToBottom) <= 0.1 ? previous : m.distanceToBottom
120
+ );
121
+ }, [debugDomAttributes, onDebugStateChange]), writeScrollTop = (0, import_react.useCallback)((nextTop) => {
122
+ let viewport = viewportRef.current;
123
+ viewport && (viewport.scrollTop = nextTop, programmaticWriteCountRef.current += 1, lastProgrammaticWriteRef.current = { at: performance.now(), top: nextTop });
124
+ }, []), cancelPendingStick = (0, import_react.useCallback)(() => {
125
+ pendingStickRafRef.current !== null && (cancelAnimationFrame(pendingStickRafRef.current), pendingStickRafRef.current = null), stickRequestedRef.current = !1;
126
+ }, []), cancelReturn = (0, import_react.useCallback)(() => {
127
+ returnRafRef.current !== null && (cancelAnimationFrame(returnRafRef.current), returnRafRef.current = null), returnAnimRef.current = null;
128
+ }, []), handleReducedMotionChange = (0, import_react.useCallback)(
129
+ (matches) => {
130
+ if (prefersReducedMotionRef.current !== matches && (prefersReducedMotionRef.current = matches, setPrefersReducedMotion(matches)), !matches || modeRef.current !== "RETURNING_SMOOTH") return;
131
+ cancelPendingStick(), cancelReturn(), setModeBoth("STICKY_INSTANT");
132
+ let viewport = viewportRef.current;
133
+ viewport && (writeScrollTop(measure(viewport).maxScrollTop), prevScrollTopRef.current = viewport.scrollTop), updateDebug();
134
+ },
135
+ [cancelPendingStick, cancelReturn, setModeBoth, updateDebug, writeScrollTop]
136
+ ), reducedMotionChangeHandlerRef = (0, import_react.useRef)(handleReducedMotionChange);
137
+ reducedMotionChangeHandlerRef.current = handleReducedMotionChange;
138
+ let interruptReturnForUserIntent = (0, import_react.useCallback)(() => {
139
+ modeRef.current === "RETURNING_SMOOTH" && (cancelPendingStick(), cancelReturn(), setModeBoth("DETACHED"), updateDebug());
140
+ }, [cancelPendingStick, cancelReturn, setModeBoth, updateDebug]), onViewportKeyDown = (0, import_react.useCallback)(
141
+ (event) => {
142
+ isScrollKeyIntent(event) && interruptReturnForUserIntent();
143
+ },
144
+ [interruptReturnForUserIntent]
145
+ ), stickToBottomInstant = (0, import_react.useCallback)(() => {
146
+ let viewport = viewportRef.current;
147
+ viewport && (stickRequestedRef.current = !0, pendingStickRafRef.current === null && (pendingStickRafRef.current = requestAnimationFrame(() => {
148
+ if (pendingStickRafRef.current = null, !stickRequestedRef.current || (stickRequestedRef.current = !1, modeRef.current !== "STICKY_INSTANT")) return;
149
+ let m = measure(viewport);
150
+ writeScrollTop(m.maxScrollTop), updateDebug();
151
+ })));
152
+ }, [updateDebug, writeScrollTop]), startReturnToBottomSmooth = (0, import_react.useCallback)(() => {
153
+ let viewport = viewportRef.current;
154
+ if (!viewport) return;
155
+ if (prefersReducedMotionRef.current) {
156
+ cancelPendingStick(), cancelReturn(), setModeBoth("STICKY_INSTANT"), writeScrollTop(measure(viewport).maxScrollTop), prevScrollTopRef.current = viewport.scrollTop, updateDebug();
157
+ return;
158
+ }
159
+ cancelPendingStick(), cancelReturn();
160
+ let initial = measure(viewport);
161
+ if (initial.distanceToBottom <= EPS_BOTTOM_PX) {
162
+ setModeBoth("STICKY_INSTANT"), stickToBottomInstant(), updateDebug();
163
+ return;
164
+ }
165
+ let durationMs = Math.max(
166
+ RETURN_MIN_MS,
167
+ Math.min(RETURN_MAX_MS, initial.distanceToBottom / RETURN_PX_PER_MS)
168
+ );
169
+ returnAnimRef.current = {
170
+ startTime: performance.now(),
171
+ startTop: initial.scrollTop,
172
+ durationMs,
173
+ settleFrames: 0
174
+ }, setModeBoth("RETURNING_SMOOTH");
175
+ let tick = (now) => {
176
+ let viewportCurrent = viewportRef.current, anim = returnAnimRef.current;
177
+ if (!viewportCurrent || !anim || modeRef.current !== "RETURNING_SMOOTH") {
178
+ cancelReturn();
179
+ return;
180
+ }
181
+ let before = measure(viewportCurrent), progress = clamp01((now - anim.startTime) / anim.durationMs), eased = easeOutCubic(progress), targetBottom = before.maxScrollTop, nextTop = anim.startTop + (targetBottom - anim.startTop) * eased;
182
+ nextTop < before.scrollTop && (nextTop = before.scrollTop), writeScrollTop(nextTop), measure(viewportCurrent).distanceToBottom <= EPS_BOTTOM_PX ? anim.settleFrames += 1 : anim.settleFrames = 0;
183
+ let doneByTime = progress >= 1, doneBySettle = anim.settleFrames >= RETURN_SETTLE_FRAMES && progress >= 0.6;
184
+ if (doneByTime && doneBySettle) {
185
+ cancelReturn(), setModeBoth("STICKY_INSTANT"), stickToBottomInstant(), updateDebug();
186
+ return;
187
+ }
188
+ updateDebug(), returnRafRef.current = requestAnimationFrame(tick);
189
+ };
190
+ returnRafRef.current = requestAnimationFrame(tick);
191
+ }, [cancelPendingStick, cancelReturn, setModeBoth, stickToBottomInstant, updateDebug, writeScrollTop]), onViewportScroll = (0, import_react.useCallback)(
192
+ (event) => {
193
+ let viewport = event.currentTarget, m = measure(viewport);
194
+ setIsOverflowing((previous) => previous === m.isOverflowing ? previous : m.isOverflowing);
195
+ let previousTop = prevScrollTopRef.current, delta = m.scrollTop - previousTop, lastWrite = lastProgrammaticWriteRef.current, isLikelyOwnWrite = lastWrite !== null && performance.now() - lastWrite.at < 60 && lastWrite !== null && Math.abs(m.scrollTop - lastWrite.top) <= 2;
196
+ modeRef.current === "RETURNING_SMOOTH" ? !isLikelyOwnWrite && delta < -DETACH_SCROLL_DELTA_PX && (cancelReturn(), setModeBoth("DETACHED")) : modeRef.current === "STICKY_INSTANT" ? !isLikelyOwnWrite && delta < -DETACH_SCROLL_DELTA_PX && m.isOverflowing && (cancelPendingStick(), setModeBoth("DETACHED")) : modeRef.current === "DETACHED" && m.distanceToBottom <= EPS_BOTTOM_PX && (setModeBoth("STICKY_INSTANT"), stickToBottomInstant()), prevScrollTopRef.current = m.scrollTop, updateDebug();
197
+ },
198
+ [cancelPendingStick, cancelReturn, setModeBoth, stickToBottomInstant, updateDebug]
199
+ );
200
+ (0, import_react.useEffect)(() => {
201
+ let viewport = viewportRef.current, content = contentRef.current;
202
+ if (!viewport || !content) return;
203
+ let observer = new ResizeObserver(() => {
204
+ let m = measure(viewport);
205
+ setIsOverflowing((previous) => previous === m.isOverflowing ? previous : m.isOverflowing), modeRef.current === "STICKY_INSTANT" && stickToBottomInstant(), updateDebug();
206
+ });
207
+ return observer.observe(content), observer.observe(viewport), () => observer.disconnect();
208
+ }, [stickToBottomInstant, updateDebug]), (0, import_react.useLayoutEffect)(() => {
209
+ let viewport = viewportRef.current;
210
+ if (!viewport) return;
211
+ setModeBoth("STICKY_INSTANT");
212
+ let initial = measure(viewport);
213
+ writeScrollTop(initial.maxScrollTop), prevScrollTopRef.current = viewport.scrollTop, setIsOverflowing(initial.isOverflowing), setDistanceToBottom(initial.distanceToBottom), updateDebug();
214
+ }, [setModeBoth, updateDebug, writeScrollTop]), (0, import_react.useLayoutEffect)(() => {
215
+ if (typeof window > "u" || typeof window.matchMedia != "function") return;
216
+ let mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"), onChange = () => reducedMotionChangeHandlerRef.current(mediaQuery.matches);
217
+ return onChange(), typeof mediaQuery.addEventListener == "function" ? (mediaQuery.addEventListener("change", onChange), () => mediaQuery.removeEventListener("change", onChange)) : (mediaQuery.addListener(onChange), () => mediaQuery.removeListener(onChange));
218
+ }, []), (0, import_react.useEffect)(() => () => {
219
+ cancelPendingStick(), cancelReturn();
220
+ }, [cancelPendingStick, cancelReturn]);
221
+ let canJump = isOverflowing && mode === "DETACHED", jumpButton = renderJumpToBottom?.({
222
+ mode,
223
+ canJump,
224
+ isOverflowing,
225
+ distanceToBottom,
226
+ jumpToBottom: startReturnToBottomSmooth
227
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultJumpButton, { canJump, jumpToBottom: startReturnToBottomSmooth }), jumpContainerClassNameBase = "pointer-events-none absolute z-20", jumpContainerStyle = {
228
+ position: "absolute",
229
+ zIndex: 20,
230
+ left: 0,
231
+ right: 0,
232
+ bottom: 12,
233
+ display: "flex",
234
+ justifyContent: "center",
235
+ opacity: canJump ? 1 : 0,
236
+ transform: prefersReducedMotion ? "none" : canJump ? "translateY(0px)" : "translateY(4px)",
237
+ transition: prefersReducedMotion ? "none" : "opacity 150ms ease, transform 150ms ease",
238
+ pointerEvents: "none"
239
+ };
240
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: joinClasses("relative h-full w-full overflow-hidden", className), children: [
241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
242
+ "div",
243
+ {
244
+ ref: viewportRef,
245
+ className: joinClasses("h-full w-full overflow-auto", viewportClassName),
246
+ onScroll: onViewportScroll,
247
+ onWheel: interruptReturnForUserIntent,
248
+ onTouchStart: interruptReturnForUserIntent,
249
+ onKeyDown: onViewportKeyDown,
250
+ "data-testid": "sticky-scroll-viewport",
251
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: contentRef, className: joinClasses("flex min-h-full w-full flex-col", contentClassName), children })
252
+ }
253
+ ),
254
+ showJumpToBottom ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
+ "div",
256
+ {
257
+ className: joinClasses(jumpContainerClassNameBase, jumpContainerClassName),
258
+ style: jumpContainerStyle,
259
+ "aria-hidden": !canJump,
260
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: canJump ? "pointer-events-auto" : "pointer-events-none", children: jumpButton })
261
+ }
262
+ ) : null
263
+ ] });
264
+ }
265
+ // Annotate the CommonJS export names for ESM import in node:
266
+ 0 && (module.exports = {
267
+ BottomStickScrollArea
268
+ });
@@ -0,0 +1,36 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type BottomStickMode = "STICKY_INSTANT" | "DETACHED" | "RETURNING_SMOOTH";
5
+ type BottomStickDebugState = {
6
+ mode: BottomStickMode;
7
+ isOverflowing: boolean;
8
+ distanceToBottom: number;
9
+ scrollTop: number;
10
+ maxScrollTop: number;
11
+ scrollHeight: number;
12
+ clientHeight: number;
13
+ programmaticWrites: number;
14
+ };
15
+ type BottomStickJumpButtonRenderState = {
16
+ mode: BottomStickMode;
17
+ canJump: boolean;
18
+ isOverflowing: boolean;
19
+ distanceToBottom: number;
20
+ jumpToBottom: () => void;
21
+ };
22
+ type BottomStickScrollAreaProps = {
23
+ children: ReactNode;
24
+ className?: string;
25
+ viewportClassName?: string;
26
+ contentClassName?: string;
27
+ showJumpToBottom?: boolean;
28
+ onDebugStateChange?: (state: BottomStickDebugState) => void;
29
+ onModeChange?: (mode: BottomStickMode) => void;
30
+ debugDomAttributes?: boolean;
31
+ renderJumpToBottom?: (state: BottomStickJumpButtonRenderState) => ReactNode;
32
+ jumpContainerClassName?: string;
33
+ };
34
+ declare function BottomStickScrollArea({ children, className, viewportClassName, contentClassName, showJumpToBottom, onDebugStateChange, onModeChange, debugDomAttributes, renderJumpToBottom, jumpContainerClassName, }: BottomStickScrollAreaProps): react_jsx_runtime.JSX.Element;
35
+
36
+ export { type BottomStickDebugState, type BottomStickJumpButtonRenderState, type BottomStickMode, BottomStickScrollArea, type BottomStickScrollAreaProps };
@@ -0,0 +1,36 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type BottomStickMode = "STICKY_INSTANT" | "DETACHED" | "RETURNING_SMOOTH";
5
+ type BottomStickDebugState = {
6
+ mode: BottomStickMode;
7
+ isOverflowing: boolean;
8
+ distanceToBottom: number;
9
+ scrollTop: number;
10
+ maxScrollTop: number;
11
+ scrollHeight: number;
12
+ clientHeight: number;
13
+ programmaticWrites: number;
14
+ };
15
+ type BottomStickJumpButtonRenderState = {
16
+ mode: BottomStickMode;
17
+ canJump: boolean;
18
+ isOverflowing: boolean;
19
+ distanceToBottom: number;
20
+ jumpToBottom: () => void;
21
+ };
22
+ type BottomStickScrollAreaProps = {
23
+ children: ReactNode;
24
+ className?: string;
25
+ viewportClassName?: string;
26
+ contentClassName?: string;
27
+ showJumpToBottom?: boolean;
28
+ onDebugStateChange?: (state: BottomStickDebugState) => void;
29
+ onModeChange?: (mode: BottomStickMode) => void;
30
+ debugDomAttributes?: boolean;
31
+ renderJumpToBottom?: (state: BottomStickJumpButtonRenderState) => ReactNode;
32
+ jumpContainerClassName?: string;
33
+ };
34
+ declare function BottomStickScrollArea({ children, className, viewportClassName, contentClassName, showJumpToBottom, onDebugStateChange, onModeChange, debugDomAttributes, renderJumpToBottom, jumpContainerClassName, }: BottomStickScrollAreaProps): react_jsx_runtime.JSX.Element;
35
+
36
+ export { type BottomStickDebugState, type BottomStickJumpButtonRenderState, type BottomStickMode, BottomStickScrollArea, type BottomStickScrollAreaProps };