@lobehub/ui 5.15.16 → 5.16.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 (37) hide show
  1. package/es/Markdown/Markdown.mjs +2 -1
  2. package/es/Markdown/Markdown.mjs.map +1 -1
  3. package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs +82 -0
  4. package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs.map +1 -0
  5. package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +127 -97
  6. package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
  7. package/es/Markdown/SyntaxMarkdown/style.mjs +1 -2
  8. package/es/Markdown/SyntaxMarkdown/style.mjs.map +1 -1
  9. package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +32 -14
  10. package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -1
  11. package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs +2 -5
  12. package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs.map +1 -1
  13. package/es/Markdown/components/MarkdownProvider.mjs +2 -1
  14. package/es/Markdown/components/MarkdownProvider.mjs.map +1 -1
  15. package/es/Markdown/index.d.mts +2 -2
  16. package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +21 -0
  17. package/es/Markdown/plugins/rehypeStreamAnimated.mjs +61 -25
  18. package/es/Markdown/plugins/rehypeStreamAnimated.mjs.map +1 -1
  19. package/es/Markdown/type.d.mts +3 -1
  20. package/es/Tabs/Tabs.mjs +6 -14
  21. package/es/Tabs/Tabs.mjs.map +1 -1
  22. package/es/base-ui/Tabs/Tabs.d.mts +8 -0
  23. package/es/base-ui/Tabs/Tabs.mjs +54 -0
  24. package/es/base-ui/Tabs/Tabs.mjs.map +1 -0
  25. package/es/base-ui/Tabs/atoms.d.mts +22 -0
  26. package/es/base-ui/Tabs/atoms.mjs +68 -0
  27. package/es/base-ui/Tabs/atoms.mjs.map +1 -0
  28. package/es/base-ui/Tabs/index.d.mts +4 -0
  29. package/es/base-ui/Tabs/style.d.mts +20 -0
  30. package/es/base-ui/Tabs/style.mjs +203 -0
  31. package/es/base-ui/Tabs/style.mjs.map +1 -0
  32. package/es/base-ui/Tabs/type.d.mts +64 -0
  33. package/es/base-ui/index.d.mts +5 -1
  34. package/es/base-ui/index.mjs +4 -1
  35. package/es/utils/getNow.mjs +8 -0
  36. package/es/utils/getNow.mjs.map +1 -0
  37. package/package.json +6 -1
@@ -1,3 +1,4 @@
1
+ import { getNow } from "../../utils/getNow.mjs";
1
2
  import { useStreamdownProfiler } from "../streamProfiler/StreamdownProfilerProvider.mjs";
2
3
  import { findOpenFenceLanguage } from "./fenceState.mjs";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -14,6 +15,7 @@ const PRESET_CONFIG = {
14
15
  maxActiveCps: 132,
15
16
  maxCps: 72,
16
17
  maxFlushCps: 280,
18
+ minCommitIntervalMs: 48,
17
19
  minCps: 18,
18
20
  settleAfterMs: 360,
19
21
  settleDrainMaxMs: 520,
@@ -30,6 +32,7 @@ const PRESET_CONFIG = {
30
32
  maxActiveCps: 180,
31
33
  maxCps: 96,
32
34
  maxFlushCps: 360,
35
+ minCommitIntervalMs: 32,
33
36
  minCps: 24,
34
37
  settleAfterMs: 260,
35
38
  settleDrainMaxMs: 360,
@@ -46,6 +49,7 @@ const PRESET_CONFIG = {
46
49
  maxActiveCps: 102,
47
50
  maxCps: 56,
48
51
  maxFlushCps: 220,
52
+ minCommitIntervalMs: 56,
49
53
  minCps: 14,
50
54
  settleAfterMs: 460,
51
55
  settleDrainMaxMs: 680,
@@ -56,11 +60,16 @@ const PRESET_CONFIG = {
56
60
  const clamp = (value, min, max) => {
57
61
  return Math.min(max, Math.max(min, value));
58
62
  };
59
- const getNow = () => {
60
- return typeof performance === "undefined" ? Date.now() : performance.now();
63
+ const MAX_COMMIT_INTERVAL_MS = 96;
64
+ const COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;
65
+ const findTailUnits = (content) => {
66
+ const boundary = content.lastIndexOf("\n\n");
67
+ return boundary === -1 ? content.length : content.length - boundary - 2;
61
68
  };
62
69
  const countChars = (text) => {
63
- return [...text].length;
70
+ let count = 0;
71
+ for (const _char of text) count += 1;
72
+ return count;
64
73
  };
65
74
  const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" } = {}) => {
66
75
  const config = PRESET_CONFIG[preset];
@@ -72,6 +81,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
72
81
  const targetCharsRef = useRef([...content]);
73
82
  const targetCountRef = useRef(targetCharsRef.current.length);
74
83
  const emaCpsRef = useRef(config.defaultCps);
84
+ const tailUnitsRef = useRef(findTailUnits(content));
75
85
  const lastInputTsRef = useRef(0);
76
86
  const lastInputCountRef = useRef(targetCountRef.current);
77
87
  const chunkSizeEmaRef = useRef(1);
@@ -117,6 +127,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
117
127
  emaCpsRef.current = config.defaultCps;
118
128
  chunkSizeEmaRef.current = 1;
119
129
  arrivalCpsEmaRef.current = config.defaultCps;
130
+ tailUnitsRef.current = findTailUnits(nextContent);
120
131
  lastInputTsRef.current = now;
121
132
  lastInputCountRef.current = chars.length;
122
133
  }, [config.defaultCps, stopScheduling]);
@@ -124,23 +135,28 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
124
135
  clearWakeTimer();
125
136
  if (rafRef.current !== null) return;
126
137
  const tick = (ts) => {
127
- const frameStart = getNow();
138
+ const targetCount = targetCountRef.current;
139
+ const displayedCount = displayedCountRef.current;
140
+ const backlog = targetCount - displayedCount;
141
+ if (backlog <= 0) {
142
+ stopFrameLoop();
143
+ return;
144
+ }
128
145
  if (lastFrameTsRef.current === null) {
129
146
  lastFrameTsRef.current = ts;
130
147
  rafRef.current = requestAnimationFrame(tick);
131
148
  return;
132
149
  }
150
+ const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS));
133
151
  const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);
134
- const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .05));
135
- lastFrameTsRef.current = ts;
136
- const targetCount = targetCountRef.current;
137
- const displayedCount = displayedCountRef.current;
138
- const backlog = targetCount - displayedCount;
139
- if (backlog <= 0) {
140
- stopFrameLoop();
152
+ if (frameIntervalMs < commitIntervalMs) {
153
+ rafRef.current = requestAnimationFrame(tick);
141
154
  return;
142
155
  }
143
- const idleMs = getNow() - lastInputTsRef.current;
156
+ const frameStart = getNow();
157
+ const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
158
+ lastFrameTsRef.current = ts;
159
+ const idleMs = frameStart - lastInputTsRef.current;
144
160
  const inputActive = idleMs <= config.activeInputWindowMs;
145
161
  const settling = !inputActive && idleMs >= config.settleAfterMs;
146
162
  const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);
@@ -210,6 +226,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
210
226
  config.maxActiveCps,
211
227
  config.maxCps,
212
228
  config.maxFlushCps,
229
+ config.minCommitIntervalMs,
213
230
  config.minCps,
214
231
  config.settleAfterMs,
215
232
  config.settleDrainMaxMs,
@@ -249,8 +266,9 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
249
266
  return;
250
267
  }
251
268
  targetContentRef.current = content;
252
- targetCharsRef.current = [...targetCharsRef.current, ...appendedChars];
269
+ targetCharsRef.current.push(...appendedChars);
253
270
  targetCountRef.current += appendedCount;
271
+ tailUnitsRef.current = findTailUnits(content);
254
272
  const deltaChars = targetCountRef.current - lastInputCountRef.current;
255
273
  const deltaMs = Math.max(1, now - lastInputTsRef.current);
256
274
  if (deltaChars > 0) {
@@ -287,6 +305,6 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
287
305
  return displayedContent;
288
306
  };
289
307
  //#endregion
290
- export { useSmoothStreamContent };
308
+ export { countChars, useSmoothStreamContent };
291
309
 
292
310
  //# sourceMappingURL=useSmoothStreamContent.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSmoothStreamContent.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\n\nimport { findOpenFenceLanguage } from './fenceState';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\n /**\n * Code-fence languages whose contents bypass smoothing entirely. While\n * the input ends inside an open fence in this set, every chunk is\n * `syncImmediate`-d to the display so downstream consumers (the\n * HtmlPreview iframe in particular) see partial HTML at the demo's\n * actual production rate instead of waiting on the smoother's\n * ~maxFlushCps budget. As soon as the fence closes — or another\n * non-bypass fence opens — smoothing resumes for the rest of the\n * stream. Default `['html']` covers the artifact case that motivated\n * this; tune via preset if you need to bypass `mermaid`, `svg`, etc.\n */\n bypassFencedLanguages: readonly string[];\n defaultCps: number;\n emaAlpha: number;\n flushCps: number;\n largeAppendChars: number;\n maxActiveCps: number;\n maxCps: number;\n maxFlushCps: number;\n minCps: number;\n settleAfterMs: number;\n settleDrainMaxMs: number;\n settleDrainMinMs: number;\n targetBufferMs: number;\n}\n\nconst DEFAULT_BYPASS_LANGUAGES = ['html'] as const;\n\nconst PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 38,\n emaAlpha: 0.2,\n flushCps: 120,\n largeAppendChars: 120,\n maxActiveCps: 132,\n maxCps: 72,\n maxFlushCps: 280,\n minCps: 18,\n settleAfterMs: 360,\n settleDrainMaxMs: 520,\n settleDrainMinMs: 180,\n targetBufferMs: 120,\n },\n realtime: {\n activeInputWindowMs: 140,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 50,\n emaAlpha: 0.3,\n flushCps: 170,\n largeAppendChars: 180,\n maxActiveCps: 180,\n maxCps: 96,\n maxFlushCps: 360,\n minCps: 24,\n settleAfterMs: 260,\n settleDrainMaxMs: 360,\n settleDrainMinMs: 140,\n targetBufferMs: 40,\n },\n silky: {\n activeInputWindowMs: 320,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 28,\n emaAlpha: 0.14,\n flushCps: 96,\n largeAppendChars: 100,\n maxActiveCps: 102,\n maxCps: 56,\n maxFlushCps: 220,\n minCps: 14,\n settleAfterMs: 460,\n settleDrainMaxMs: 680,\n settleDrainMinMs: 240,\n targetBufferMs: 170,\n },\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n return Math.min(max, Math.max(min, value));\n};\n\nconst getNow = () => {\n return typeof performance === 'undefined' ? Date.now() : performance.now();\n};\n\nexport const countChars = (text: string): number => {\n return [...text].length;\n};\n\ninterface UseSmoothStreamContentOptions {\n enabled?: boolean;\n preset?: StreamSmoothingPreset;\n}\n\nexport const useSmoothStreamContent = (\n content: string,\n { enabled = true, preset = 'balanced' }: UseSmoothStreamContentOptions = {},\n): string => {\n const config = PRESET_CONFIG[preset];\n const profiler = useStreamdownProfiler();\n const [displayedContent, setDisplayedContent] = useState(content);\n\n const displayedContentRef = useRef(content);\n const displayedCountRef = useRef(countChars(content));\n\n const targetContentRef = useRef(content);\n const targetCharsRef = useRef([...content]);\n const targetCountRef = useRef(targetCharsRef.current.length);\n\n const emaCpsRef = useRef(config.defaultCps);\n const lastInputTsRef = useRef(0);\n const lastInputCountRef = useRef(targetCountRef.current);\n const chunkSizeEmaRef = useRef(1);\n const arrivalCpsEmaRef = useRef(config.defaultCps);\n\n const rafRef = useRef<number | null>(null);\n const lastFrameTsRef = useRef<number | null>(null);\n const wakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearWakeTimer = useCallback(() => {\n if (wakeTimerRef.current !== null) {\n clearTimeout(wakeTimerRef.current);\n wakeTimerRef.current = null;\n }\n }, []);\n\n const stopFrameLoop = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameTsRef.current = null;\n }, []);\n\n const stopScheduling = useCallback(() => {\n stopFrameLoop();\n clearWakeTimer();\n }, [clearWakeTimer, stopFrameLoop]);\n\n const startFrameLoopRef = useRef<() => void>(() => {});\n\n const scheduleFrameWake = useCallback(\n (delayMs: number) => {\n clearWakeTimer();\n\n wakeTimerRef.current = setTimeout(\n () => {\n wakeTimerRef.current = null;\n startFrameLoopRef.current();\n },\n Math.max(1, Math.ceil(delayMs)),\n );\n },\n [clearWakeTimer],\n );\n\n const syncImmediate = useCallback(\n (nextContent: string) => {\n stopScheduling();\n\n const chars = [...nextContent];\n const now = getNow();\n\n targetContentRef.current = nextContent;\n targetCharsRef.current = chars;\n targetCountRef.current = chars.length;\n\n displayedContentRef.current = nextContent;\n displayedCountRef.current = chars.length;\n setDisplayedContent(nextContent);\n\n emaCpsRef.current = config.defaultCps;\n chunkSizeEmaRef.current = 1;\n arrivalCpsEmaRef.current = config.defaultCps;\n lastInputTsRef.current = now;\n lastInputCountRef.current = chars.length;\n },\n [config.defaultCps, stopScheduling],\n );\n\n const startFrameLoop = useCallback(() => {\n clearWakeTimer();\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n const frameStart = getNow();\n\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);\n const dtSeconds = Math.max(0.001, Math.min(frameIntervalMs / 1000, 0.05));\n lastFrameTsRef.current = ts;\n\n const targetCount = targetCountRef.current;\n const displayedCount = displayedCountRef.current;\n const backlog = targetCount - displayedCount;\n\n if (backlog <= 0) {\n stopFrameLoop();\n return;\n }\n\n const now = getNow();\n const idleMs = now - lastInputTsRef.current;\n const inputActive = idleMs <= config.activeInputWindowMs;\n const settling = !inputActive && idleMs >= config.settleAfterMs;\n\n const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);\n const baseLagChars = Math.max(1, Math.round((baseCps * config.targetBufferMs) / 1000));\n const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);\n const targetLagChars = inputActive\n ? Math.round(\n clamp(baseLagChars + chunkSizeEmaRef.current * 0.35, baseLagChars, lagUpperBound),\n )\n : 0;\n const desiredDisplayed = Math.max(0, targetCount - targetLagChars);\n\n let currentCps: number;\n if (inputActive) {\n const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;\n const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;\n const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);\n const combinedPressure = clamp(\n backlogPressure * 0.6 + chunkPressure * 0.25 + arrivalPressure * 0.15,\n 1,\n 4.5,\n );\n const activeCap = clamp(\n config.maxActiveCps + chunkSizeEmaRef.current * 6,\n config.maxActiveCps,\n config.maxFlushCps,\n );\n currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);\n } else if (settling) {\n // If upstream likely ended, cap the remaining tail duration so\n // we do not keep replaying old backlog for seconds.\n const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);\n const settleCps = (backlog * 1000) / drainTargetMs;\n currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);\n } else {\n const idleFlushCps = Math.max(\n config.flushCps,\n baseCps * 1.8,\n arrivalCpsEmaRef.current * 0.8,\n );\n currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);\n }\n\n const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;\n const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * 0.9;\n const minRevealChars = inputActive ? (urgentBacklog || burstyInput ? 2 : 1) : 2;\n let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));\n\n if (inputActive) {\n const shortfall = desiredDisplayed - displayedCount;\n if (shortfall <= 0) {\n stopFrameLoop();\n scheduleFrameWake(config.activeInputWindowMs - idleMs);\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: 0,\n settling,\n });\n return;\n }\n revealChars = Math.min(revealChars, shortfall, backlog);\n } else {\n revealChars = Math.min(revealChars, backlog);\n }\n\n const nextCount = displayedCount + revealChars;\n const segment = targetCharsRef.current.slice(displayedCount, nextCount).join('');\n\n if (segment) {\n const nextDisplayed = displayedContentRef.current + segment;\n displayedContentRef.current = nextDisplayed;\n displayedCountRef.current = nextCount;\n setDisplayedContent(nextDisplayed);\n } else {\n displayedContentRef.current = targetContentRef.current;\n displayedCountRef.current = targetCount;\n setDisplayedContent(targetContentRef.current);\n }\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: segment ? revealChars : backlog,\n settling,\n });\n\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\n clearWakeTimer,\n config.activeInputWindowMs,\n config.flushCps,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n config.settleAfterMs,\n config.settleDrainMaxMs,\n config.settleDrainMinMs,\n config.targetBufferMs,\n scheduleFrameWake,\n stopFrameLoop,\n ]);\n startFrameLoopRef.current = startFrameLoop;\n\n useEffect(() => {\n if (!enabled) {\n syncImmediate(content);\n return;\n }\n\n const prevTargetContent = targetContentRef.current;\n if (content === prevTargetContent) return;\n\n const now = getNow();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n // Bypass smoothing entirely while the input ends inside an open\n // fence whose language is opted out — see the preset config.\n // Without this, a 5 KB inline `<style>` block can keep `</style>`\n // (and therefore the HtmlPreview head-close trigger) trapped in the\n // smoother's buffer for tens of seconds, leaving the iframe pinned\n // on the loading placeholder while the artifact \"looks stuck\".\n if (config.bypassFencedLanguages.length > 0) {\n const openLang = findOpenFenceLanguage(content);\n if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {\n syncImmediate(content);\n return;\n }\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\n\n profiler?.recordInputAppend({\n appendedChars: appendedCount,\n contentLength: countChars(content),\n });\n\n if (appendedCount > config.largeAppendChars) {\n syncImmediate(content);\n return;\n }\n\n targetContentRef.current = content;\n targetCharsRef.current = [...targetCharsRef.current, ...appendedChars];\n targetCountRef.current += appendedCount;\n\n const deltaChars = targetCountRef.current - lastInputCountRef.current;\n const deltaMs = Math.max(1, now - lastInputTsRef.current);\n\n if (deltaChars > 0) {\n const instantCps = (deltaChars * 1000) / deltaMs;\n const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);\n const chunkEmaAlpha = 0.35;\n chunkSizeEmaRef.current =\n chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;\n arrivalCpsEmaRef.current =\n arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;\n\n const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);\n emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;\n }\n\n lastInputTsRef.current = now;\n lastInputCountRef.current = targetCountRef.current;\n\n startFrameLoop();\n }, [\n config.bypassFencedLanguages,\n config.emaAlpha,\n config.largeAppendChars,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n content,\n enabled,\n startFrameLoop,\n syncImmediate,\n profiler,\n ]);\n\n useEffect(() => {\n return () => {\n stopScheduling();\n };\n }, [stopScheduling]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSmoothStreamContent.mjs","names":["now"],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\nimport { getNow } from '@/utils/getNow';\n\nimport { findOpenFenceLanguage } from './fenceState';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\n /**\n * Code-fence languages whose contents bypass smoothing entirely. While\n * the input ends inside an open fence in this set, every chunk is\n * `syncImmediate`-d to the display so downstream consumers (the\n * HtmlPreview iframe in particular) see partial HTML at the demo's\n * actual production rate instead of waiting on the smoother's\n * ~maxFlushCps budget. As soon as the fence closes — or another\n * non-bypass fence opens — smoothing resumes for the rest of the\n * stream. Default `['html']` covers the artifact case that motivated\n * this; tune via preset if you need to bypass `mermaid`, `svg`, etc.\n */\n bypassFencedLanguages: readonly string[];\n defaultCps: number;\n emaAlpha: number;\n flushCps: number;\n largeAppendChars: number;\n maxActiveCps: number;\n maxCps: number;\n maxFlushCps: number;\n minCommitIntervalMs: number;\n minCps: number;\n settleAfterMs: number;\n settleDrainMaxMs: number;\n settleDrainMinMs: number;\n targetBufferMs: number;\n}\n\nconst DEFAULT_BYPASS_LANGUAGES = ['html'] as const;\n\nconst PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 38,\n emaAlpha: 0.2,\n flushCps: 120,\n largeAppendChars: 120,\n maxActiveCps: 132,\n maxCps: 72,\n maxFlushCps: 280,\n minCommitIntervalMs: 48,\n minCps: 18,\n settleAfterMs: 360,\n settleDrainMaxMs: 520,\n settleDrainMinMs: 180,\n targetBufferMs: 120,\n },\n realtime: {\n activeInputWindowMs: 140,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 50,\n emaAlpha: 0.3,\n flushCps: 170,\n largeAppendChars: 180,\n maxActiveCps: 180,\n maxCps: 96,\n maxFlushCps: 360,\n minCommitIntervalMs: 32,\n minCps: 24,\n settleAfterMs: 260,\n settleDrainMaxMs: 360,\n settleDrainMinMs: 140,\n targetBufferMs: 40,\n },\n silky: {\n activeInputWindowMs: 320,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 28,\n emaAlpha: 0.14,\n flushCps: 96,\n largeAppendChars: 100,\n maxActiveCps: 102,\n maxCps: 56,\n maxFlushCps: 220,\n minCommitIntervalMs: 56,\n minCps: 14,\n settleAfterMs: 460,\n settleDrainMaxMs: 680,\n settleDrainMinMs: 240,\n targetBufferMs: 170,\n },\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n return Math.min(max, Math.max(min, value));\n};\n\n// Every reveal commit re-parses and re-wraps the entire trailing block, so\n// the per-commit cost grows linearly with how far the content is from the\n// last block boundary. Widen the commit interval as that distance grows —\n// long paragraphs/code fences keep total work bounded while short blocks\n// stay at the preset's snappy interval. Per-char animation-delay stagger\n// hides the lower commit rate.\nconst MAX_COMMIT_INTERVAL_MS = 96;\nconst COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;\n\nconst findTailUnits = (content: string): number => {\n const boundary = content.lastIndexOf('\\n\\n');\n return boundary === -1 ? content.length : content.length - boundary - 2;\n};\n\nexport const countChars = (text: string): number => {\n let count = 0;\n for (const _char of text) count += 1;\n return count;\n};\n\ninterface UseSmoothStreamContentOptions {\n enabled?: boolean;\n preset?: StreamSmoothingPreset;\n}\n\nexport const useSmoothStreamContent = (\n content: string,\n { enabled = true, preset = 'balanced' }: UseSmoothStreamContentOptions = {},\n): string => {\n const config = PRESET_CONFIG[preset];\n const profiler = useStreamdownProfiler();\n const [displayedContent, setDisplayedContent] = useState(content);\n\n const displayedContentRef = useRef(content);\n const displayedCountRef = useRef(countChars(content));\n\n const targetContentRef = useRef(content);\n const targetCharsRef = useRef([...content]);\n const targetCountRef = useRef(targetCharsRef.current.length);\n\n const emaCpsRef = useRef(config.defaultCps);\n const tailUnitsRef = useRef(findTailUnits(content));\n const lastInputTsRef = useRef(0);\n const lastInputCountRef = useRef(targetCountRef.current);\n const chunkSizeEmaRef = useRef(1);\n const arrivalCpsEmaRef = useRef(config.defaultCps);\n\n const rafRef = useRef<number | null>(null);\n const lastFrameTsRef = useRef<number | null>(null);\n const wakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearWakeTimer = useCallback(() => {\n if (wakeTimerRef.current !== null) {\n clearTimeout(wakeTimerRef.current);\n wakeTimerRef.current = null;\n }\n }, []);\n\n const stopFrameLoop = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameTsRef.current = null;\n }, []);\n\n const stopScheduling = useCallback(() => {\n stopFrameLoop();\n clearWakeTimer();\n }, [clearWakeTimer, stopFrameLoop]);\n\n const startFrameLoopRef = useRef<() => void>(() => {});\n\n const scheduleFrameWake = useCallback(\n (delayMs: number) => {\n clearWakeTimer();\n\n wakeTimerRef.current = setTimeout(\n () => {\n wakeTimerRef.current = null;\n startFrameLoopRef.current();\n },\n Math.max(1, Math.ceil(delayMs)),\n );\n },\n [clearWakeTimer],\n );\n\n const syncImmediate = useCallback(\n (nextContent: string) => {\n stopScheduling();\n\n const chars = [...nextContent];\n const now = getNow();\n\n targetContentRef.current = nextContent;\n targetCharsRef.current = chars;\n targetCountRef.current = chars.length;\n\n displayedContentRef.current = nextContent;\n displayedCountRef.current = chars.length;\n setDisplayedContent(nextContent);\n\n emaCpsRef.current = config.defaultCps;\n chunkSizeEmaRef.current = 1;\n arrivalCpsEmaRef.current = config.defaultCps;\n tailUnitsRef.current = findTailUnits(nextContent);\n lastInputTsRef.current = now;\n lastInputCountRef.current = chars.length;\n },\n [config.defaultCps, stopScheduling],\n );\n\n const startFrameLoop = useCallback(() => {\n clearWakeTimer();\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n const targetCount = targetCountRef.current;\n const displayedCount = displayedCountRef.current;\n const backlog = targetCount - displayedCount;\n\n if (backlog <= 0) {\n stopFrameLoop();\n return;\n }\n\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n // Reveal commits are throttled below the display refresh rate: each\n // commit re-renders the tail block and re-runs remend + lexing, so\n // committing at 60-120fps burns CPU without visible benefit — the\n // per-char stagger inside a commit is carried by animation-delay.\n const commitIntervalMs = Math.min(\n MAX_COMMIT_INTERVAL_MS,\n config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS),\n );\n const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);\n if (frameIntervalMs < commitIntervalMs) {\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const frameStart = getNow();\n const dtSeconds = Math.max(0.001, Math.min(frameIntervalMs / 1000, 0.12));\n lastFrameTsRef.current = ts;\n\n const now = frameStart;\n const idleMs = now - lastInputTsRef.current;\n const inputActive = idleMs <= config.activeInputWindowMs;\n const settling = !inputActive && idleMs >= config.settleAfterMs;\n\n const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);\n const baseLagChars = Math.max(1, Math.round((baseCps * config.targetBufferMs) / 1000));\n const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);\n const targetLagChars = inputActive\n ? Math.round(\n clamp(baseLagChars + chunkSizeEmaRef.current * 0.35, baseLagChars, lagUpperBound),\n )\n : 0;\n const desiredDisplayed = Math.max(0, targetCount - targetLagChars);\n\n let currentCps: number;\n if (inputActive) {\n const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;\n const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;\n const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);\n const combinedPressure = clamp(\n backlogPressure * 0.6 + chunkPressure * 0.25 + arrivalPressure * 0.15,\n 1,\n 4.5,\n );\n const activeCap = clamp(\n config.maxActiveCps + chunkSizeEmaRef.current * 6,\n config.maxActiveCps,\n config.maxFlushCps,\n );\n currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);\n } else if (settling) {\n // If upstream likely ended, cap the remaining tail duration so\n // we do not keep replaying old backlog for seconds.\n const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);\n const settleCps = (backlog * 1000) / drainTargetMs;\n currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);\n } else {\n const idleFlushCps = Math.max(\n config.flushCps,\n baseCps * 1.8,\n arrivalCpsEmaRef.current * 0.8,\n );\n currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);\n }\n\n const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;\n const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * 0.9;\n const minRevealChars = inputActive ? (urgentBacklog || burstyInput ? 2 : 1) : 2;\n let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));\n\n if (inputActive) {\n const shortfall = desiredDisplayed - displayedCount;\n if (shortfall <= 0) {\n stopFrameLoop();\n scheduleFrameWake(config.activeInputWindowMs - idleMs);\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: 0,\n settling,\n });\n return;\n }\n revealChars = Math.min(revealChars, shortfall, backlog);\n } else {\n revealChars = Math.min(revealChars, backlog);\n }\n\n const nextCount = displayedCount + revealChars;\n const segment = targetCharsRef.current.slice(displayedCount, nextCount).join('');\n\n if (segment) {\n const nextDisplayed = displayedContentRef.current + segment;\n displayedContentRef.current = nextDisplayed;\n displayedCountRef.current = nextCount;\n setDisplayedContent(nextDisplayed);\n } else {\n displayedContentRef.current = targetContentRef.current;\n displayedCountRef.current = targetCount;\n setDisplayedContent(targetContentRef.current);\n }\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: segment ? revealChars : backlog,\n settling,\n });\n\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\n clearWakeTimer,\n config.activeInputWindowMs,\n config.flushCps,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCommitIntervalMs,\n config.minCps,\n config.settleAfterMs,\n config.settleDrainMaxMs,\n config.settleDrainMinMs,\n config.targetBufferMs,\n scheduleFrameWake,\n stopFrameLoop,\n ]);\n startFrameLoopRef.current = startFrameLoop;\n\n useEffect(() => {\n if (!enabled) {\n syncImmediate(content);\n return;\n }\n\n const prevTargetContent = targetContentRef.current;\n if (content === prevTargetContent) return;\n\n const now = getNow();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n // Bypass smoothing entirely while the input ends inside an open\n // fence whose language is opted out — see the preset config.\n // Without this, a 5 KB inline `<style>` block can keep `</style>`\n // (and therefore the HtmlPreview head-close trigger) trapped in the\n // smoother's buffer for tens of seconds, leaving the iframe pinned\n // on the loading placeholder while the artifact \"looks stuck\".\n if (config.bypassFencedLanguages.length > 0) {\n const openLang = findOpenFenceLanguage(content);\n if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {\n syncImmediate(content);\n return;\n }\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\n\n profiler?.recordInputAppend({\n appendedChars: appendedCount,\n contentLength: countChars(content),\n });\n\n if (appendedCount > config.largeAppendChars) {\n syncImmediate(content);\n return;\n }\n\n targetContentRef.current = content;\n targetCharsRef.current.push(...appendedChars);\n targetCountRef.current += appendedCount;\n\n tailUnitsRef.current = findTailUnits(content);\n\n const deltaChars = targetCountRef.current - lastInputCountRef.current;\n const deltaMs = Math.max(1, now - lastInputTsRef.current);\n\n if (deltaChars > 0) {\n const instantCps = (deltaChars * 1000) / deltaMs;\n const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);\n const chunkEmaAlpha = 0.35;\n chunkSizeEmaRef.current =\n chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;\n arrivalCpsEmaRef.current =\n arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;\n\n const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);\n emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;\n }\n\n lastInputTsRef.current = now;\n lastInputCountRef.current = targetCountRef.current;\n\n startFrameLoop();\n }, [\n config.bypassFencedLanguages,\n config.emaAlpha,\n config.largeAppendChars,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n content,\n enabled,\n startFrameLoop,\n syncImmediate,\n profiler,\n ]);\n\n useEffect(() => {\n return () => {\n stopScheduling();\n };\n }, [stopScheduling]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
@@ -1,12 +1,9 @@
1
+ import { countChars } from "./useSmoothStreamContent.mjs";
1
2
  import { useCallback, useEffect, useRef, useState } from "react";
2
3
  //#region src/Markdown/SyntaxMarkdown/useStreamQueue.ts
3
4
  const BASE_DELAY = 18;
4
5
  const ACCELERATION_FACTOR = .3;
5
6
  const MAX_BLOCK_DURATION = 3e3;
6
- const FADE_DURATION = 280;
7
- function countChars(text) {
8
- return [...text].length;
9
- }
10
7
  function computeCharDelay(queueLength, charCount) {
11
8
  let delay = BASE_DELAY / (1 + queueLength * ACCELERATION_FACTOR);
12
9
  delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));
@@ -64,7 +61,7 @@ function useStreamQueue(blocks) {
64
61
  timerRef.current = null;
65
62
  }
66
63
  if (animatingIndex < 0) return;
67
- const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + FADE_DURATION;
64
+ const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + 180;
68
65
  timerRef.current = setTimeout(onAnimationDone, totalTime);
69
66
  return () => {
70
67
  if (timerRef.current) {
@@ -1 +1 @@
1
- {"version":3,"file":"useStreamQueue.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useStreamQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface BlockInfo {\n content: string;\n startOffset: number;\n}\n\nexport type BlockState = 'revealed' | 'animating' | 'streaming' | 'queued';\n\nconst BASE_DELAY = 18;\nconst ACCELERATION_FACTOR = 0.3;\nconst MAX_BLOCK_DURATION = 3000;\nconst FADE_DURATION = 280;\n\nfunction countChars(text: string): number {\n return [...text].length;\n}\n\nfunction computeCharDelay(queueLength: number, charCount: number): number {\n const acceleration = 1 + queueLength * ACCELERATION_FACTOR;\n let delay = BASE_DELAY / acceleration;\n delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));\n return delay;\n}\n\nexport interface UseStreamQueueReturn {\n charDelay: number;\n getBlockState: (index: number) => BlockState;\n queueLength: number;\n}\n\nexport function useStreamQueue(blocks: BlockInfo[]): UseStreamQueueReturn {\n const [revealedCount, setRevealedCount] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const prevBlocksLenRef = useRef(0);\n const minRevealedRef = useRef(0);\n\n // Synchronous auto-reveal during render.\n // When blocks grow, the previous tail (streaming block) is instantly\n // promoted to revealed — its chars are already visible via stream-mode\n // animation. This runs during render (not in effect) so there is NO\n // intermediate frame where the old streaming block enters 'animating'\n // state and gets stagger plugins that would restart its animations.\n if (blocks.length === 0 && prevBlocksLenRef.current !== 0) {\n minRevealedRef.current = 0;\n }\n if (blocks.length > prevBlocksLenRef.current && prevBlocksLenRef.current > 0) {\n const prevTail = prevBlocksLenRef.current - 1;\n minRevealedRef.current = Math.max(minRevealedRef.current, prevTail + 1);\n }\n prevBlocksLenRef.current = blocks.length;\n\n // State reset when stream restarts (blocks empty)\n useEffect(() => {\n if (blocks.length === 0) {\n setRevealedCount(0);\n minRevealedRef.current = 0;\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [blocks.length]);\n\n const effectiveRevealedCount = Math.max(revealedCount, minRevealedRef.current);\n const tailIndex = blocks.length - 1;\n\n const getBlockState = useCallback(\n (index: number): BlockState => {\n if (index < effectiveRevealedCount) return 'revealed';\n if (index === effectiveRevealedCount && index < tailIndex) return 'animating';\n if (index === effectiveRevealedCount && index === tailIndex) return 'streaming';\n return 'queued';\n },\n [effectiveRevealedCount, tailIndex],\n );\n\n const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);\n\n const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;\n const animatingCharCount =\n animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? '') : 0;\n\n const streamingIndex = animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;\n const activeIndex = animatingIndex >= 0 ? animatingIndex : streamingIndex;\n const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? '') : 0;\n\n // Freeze charDelay when entering a new active block (animating or streaming)\n const frozenRef = useRef({ delay: BASE_DELAY, index: -1 });\n if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) {\n frozenRef.current = {\n delay: computeCharDelay(queueLength, activeCharCount),\n index: activeIndex,\n };\n }\n const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;\n\n const onAnimationDone = useCallback(() => {\n setRevealedCount(effectiveRevealedCount + 1);\n }, [effectiveRevealedCount]);\n\n useEffect(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n\n if (animatingIndex < 0) return;\n\n const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + FADE_DURATION;\n timerRef.current = setTimeout(onAnimationDone, totalTime);\n\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, [animatingIndex, animatingCharCount, charDelay, onAnimationDone]);\n\n return { charDelay, getBlockState, queueLength };\n}\n"],"mappings":";;AASA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AAEtB,SAAS,WAAW,MAAsB;AACxC,QAAO,CAAC,GAAG,KAAK,CAAC;;AAGnB,SAAS,iBAAiB,aAAqB,WAA2B;CAExE,IAAI,QAAQ,cADS,IAAI,cAAc;AAEvC,SAAQ,KAAK,IAAI,OAAO,qBAAqB,KAAK,IAAI,WAAW,EAAE,CAAC;AACpE,QAAO;;AAST,SAAgB,eAAe,QAA2C;CACxE,MAAM,CAAC,eAAe,oBAAoB,SAAS,EAAE;CACrD,MAAM,WAAW,OAA6C,KAAK;CACnE,MAAM,mBAAmB,OAAO,EAAE;CAClC,MAAM,iBAAiB,OAAO,EAAE;AAQhC,KAAI,OAAO,WAAW,KAAK,iBAAiB,YAAY,EACtD,gBAAe,UAAU;AAE3B,KAAI,OAAO,SAAS,iBAAiB,WAAW,iBAAiB,UAAU,GAAG;EAC5E,MAAM,WAAW,iBAAiB,UAAU;AAC5C,iBAAe,UAAU,KAAK,IAAI,eAAe,SAAS,WAAW,EAAE;;AAEzE,kBAAiB,UAAU,OAAO;AAGlC,iBAAgB;AACd,MAAI,OAAO,WAAW,GAAG;AACvB,oBAAiB,EAAE;AACnB,kBAAe,UAAU;AACzB,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB,CAAC,OAAO,OAAO,CAAC;CAEnB,MAAM,yBAAyB,KAAK,IAAI,eAAe,eAAe,QAAQ;CAC9E,MAAM,YAAY,OAAO,SAAS;CAElC,MAAM,gBAAgB,aACnB,UAA8B;AAC7B,MAAI,QAAQ,uBAAwB,QAAO;AAC3C,MAAI,UAAU,0BAA0B,QAAQ,UAAW,QAAO;AAClE,MAAI,UAAU,0BAA0B,UAAU,UAAW,QAAO;AACpE,SAAO;IAET,CAAC,wBAAwB,UAAU,CACpC;CAED,MAAM,cAAc,KAAK,IAAI,GAAG,YAAY,yBAAyB,EAAE;CAEvE,MAAM,iBAAiB,yBAAyB,YAAY,yBAAyB;CACrF,MAAM,qBACJ,kBAAkB,IAAI,WAAW,OAAO,iBAAiB,WAAW,GAAG,GAAG;CAG5E,MAAM,cAAc,kBAAkB,IAAI,iBADnB,iBAAiB,KAAK,aAAa,yBAAyB,YAAY;CAE/F,MAAM,kBAAkB,eAAe,IAAI,WAAW,OAAO,cAAc,WAAW,GAAG,GAAG;CAG5F,MAAM,YAAY,OAAO;EAAE,OAAO;EAAY,OAAO;EAAI,CAAC;AAC1D,KAAI,eAAe,KAAK,gBAAgB,UAAU,QAAQ,MACxD,WAAU,UAAU;EAClB,OAAO,iBAAiB,aAAa,gBAAgB;EACrD,OAAO;EACR;CAEH,MAAM,YAAY,eAAe,IAAI,UAAU,QAAQ,QAAQ;CAE/D,MAAM,kBAAkB,kBAAkB;AACxC,mBAAiB,yBAAyB,EAAE;IAC3C,CAAC,uBAAuB,CAAC;AAE5B,iBAAgB;AACd,MAAI,SAAS,SAAS;AACpB,gBAAa,SAAS,QAAQ;AAC9B,YAAS,UAAU;;AAGrB,MAAI,iBAAiB,EAAG;EAExB,MAAM,YAAY,KAAK,IAAI,IAAI,qBAAqB,KAAK,UAAU,GAAG;AACtE,WAAS,UAAU,WAAW,iBAAiB,UAAU;AAEzD,eAAa;AACX,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB;EAAC;EAAgB;EAAoB;EAAW;EAAgB,CAAC;AAEpE,QAAO;EAAE;EAAW;EAAe;EAAa"}
1
+ {"version":3,"file":"useStreamQueue.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useStreamQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { STREAM_FADE_DURATION } from './style';\nimport { countChars } from './useSmoothStreamContent';\n\nexport interface BlockInfo {\n content: string;\n startOffset: number;\n}\n\nexport type BlockState = 'revealed' | 'animating' | 'streaming' | 'queued';\n\nconst BASE_DELAY = 18;\nconst ACCELERATION_FACTOR = 0.3;\nconst MAX_BLOCK_DURATION = 3000;\n\nfunction computeCharDelay(queueLength: number, charCount: number): number {\n const acceleration = 1 + queueLength * ACCELERATION_FACTOR;\n let delay = BASE_DELAY / acceleration;\n delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));\n return delay;\n}\n\nexport interface UseStreamQueueReturn {\n charDelay: number;\n getBlockState: (index: number) => BlockState;\n queueLength: number;\n}\n\nexport function useStreamQueue(blocks: BlockInfo[]): UseStreamQueueReturn {\n const [revealedCount, setRevealedCount] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const prevBlocksLenRef = useRef(0);\n const minRevealedRef = useRef(0);\n\n // Synchronous auto-reveal during render.\n // When blocks grow, the previous tail (streaming block) is instantly\n // promoted to revealed — its chars are already visible via stream-mode\n // animation. This runs during render (not in effect) so there is NO\n // intermediate frame where the old streaming block enters 'animating'\n // state and gets stagger plugins that would restart its animations.\n if (blocks.length === 0 && prevBlocksLenRef.current !== 0) {\n minRevealedRef.current = 0;\n }\n if (blocks.length > prevBlocksLenRef.current && prevBlocksLenRef.current > 0) {\n const prevTail = prevBlocksLenRef.current - 1;\n minRevealedRef.current = Math.max(minRevealedRef.current, prevTail + 1);\n }\n prevBlocksLenRef.current = blocks.length;\n\n // State reset when stream restarts (blocks empty)\n useEffect(() => {\n if (blocks.length === 0) {\n setRevealedCount(0);\n minRevealedRef.current = 0;\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [blocks.length]);\n\n const effectiveRevealedCount = Math.max(revealedCount, minRevealedRef.current);\n const tailIndex = blocks.length - 1;\n\n const getBlockState = useCallback(\n (index: number): BlockState => {\n if (index < effectiveRevealedCount) return 'revealed';\n if (index === effectiveRevealedCount && index < tailIndex) return 'animating';\n if (index === effectiveRevealedCount && index === tailIndex) return 'streaming';\n return 'queued';\n },\n [effectiveRevealedCount, tailIndex],\n );\n\n const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);\n\n const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;\n const animatingCharCount =\n animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? '') : 0;\n\n const streamingIndex = animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;\n const activeIndex = animatingIndex >= 0 ? animatingIndex : streamingIndex;\n const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? '') : 0;\n\n // Freeze charDelay when entering a new active block (animating or streaming)\n const frozenRef = useRef({ delay: BASE_DELAY, index: -1 });\n if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) {\n frozenRef.current = {\n delay: computeCharDelay(queueLength, activeCharCount),\n index: activeIndex,\n };\n }\n const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;\n\n const onAnimationDone = useCallback(() => {\n setRevealedCount(effectiveRevealedCount + 1);\n }, [effectiveRevealedCount]);\n\n useEffect(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n\n if (animatingIndex < 0) return;\n\n const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + STREAM_FADE_DURATION;\n timerRef.current = setTimeout(onAnimationDone, totalTime);\n\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, [animatingIndex, animatingCharCount, charDelay, onAnimationDone]);\n\n return { charDelay, getBlockState, queueLength };\n}\n"],"mappings":";;;AAYA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAE3B,SAAS,iBAAiB,aAAqB,WAA2B;CAExE,IAAI,QAAQ,cADS,IAAI,cAAc;AAEvC,SAAQ,KAAK,IAAI,OAAO,qBAAqB,KAAK,IAAI,WAAW,EAAE,CAAC;AACpE,QAAO;;AAST,SAAgB,eAAe,QAA2C;CACxE,MAAM,CAAC,eAAe,oBAAoB,SAAS,EAAE;CACrD,MAAM,WAAW,OAA6C,KAAK;CACnE,MAAM,mBAAmB,OAAO,EAAE;CAClC,MAAM,iBAAiB,OAAO,EAAE;AAQhC,KAAI,OAAO,WAAW,KAAK,iBAAiB,YAAY,EACtD,gBAAe,UAAU;AAE3B,KAAI,OAAO,SAAS,iBAAiB,WAAW,iBAAiB,UAAU,GAAG;EAC5E,MAAM,WAAW,iBAAiB,UAAU;AAC5C,iBAAe,UAAU,KAAK,IAAI,eAAe,SAAS,WAAW,EAAE;;AAEzE,kBAAiB,UAAU,OAAO;AAGlC,iBAAgB;AACd,MAAI,OAAO,WAAW,GAAG;AACvB,oBAAiB,EAAE;AACnB,kBAAe,UAAU;AACzB,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB,CAAC,OAAO,OAAO,CAAC;CAEnB,MAAM,yBAAyB,KAAK,IAAI,eAAe,eAAe,QAAQ;CAC9E,MAAM,YAAY,OAAO,SAAS;CAElC,MAAM,gBAAgB,aACnB,UAA8B;AAC7B,MAAI,QAAQ,uBAAwB,QAAO;AAC3C,MAAI,UAAU,0BAA0B,QAAQ,UAAW,QAAO;AAClE,MAAI,UAAU,0BAA0B,UAAU,UAAW,QAAO;AACpE,SAAO;IAET,CAAC,wBAAwB,UAAU,CACpC;CAED,MAAM,cAAc,KAAK,IAAI,GAAG,YAAY,yBAAyB,EAAE;CAEvE,MAAM,iBAAiB,yBAAyB,YAAY,yBAAyB;CACrF,MAAM,qBACJ,kBAAkB,IAAI,WAAW,OAAO,iBAAiB,WAAW,GAAG,GAAG;CAG5E,MAAM,cAAc,kBAAkB,IAAI,iBADnB,iBAAiB,KAAK,aAAa,yBAAyB,YAAY;CAE/F,MAAM,kBAAkB,eAAe,IAAI,WAAW,OAAO,cAAc,WAAW,GAAG,GAAG;CAG5F,MAAM,YAAY,OAAO;EAAE,OAAO;EAAY,OAAO;EAAI,CAAC;AAC1D,KAAI,eAAe,KAAK,gBAAgB,UAAU,QAAQ,MACxD,WAAU,UAAU;EAClB,OAAO,iBAAiB,aAAa,gBAAgB;EACrD,OAAO;EACR;CAEH,MAAM,YAAY,eAAe,IAAI,UAAU,QAAQ,QAAQ;CAE/D,MAAM,kBAAkB,kBAAkB;AACxC,mBAAiB,yBAAyB,EAAE;IAC3C,CAAC,uBAAuB,CAAC;AAE5B,iBAAgB;AACd,MAAI,SAAS,SAAS;AACpB,gBAAa,SAAS,QAAQ;AAC9B,YAAS,UAAU;;AAGrB,MAAI,iBAAiB,EAAG;EAExB,MAAM,YAAY,KAAK,IAAI,IAAI,qBAAqB,KAAK,UAAU,GAAA;AACnE,WAAS,UAAU,WAAW,iBAAiB,UAAU;AAEzD,eAAa;AACX,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB;EAAC;EAAgB;EAAoB;EAAW;EAAgB,CAAC;AAEpE,QAAO;EAAE;EAAW;EAAe;EAAa"}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
+ import { useStableValue } from "../../hooks/useStableValue.mjs";
2
3
  import { createContext, memo, use } from "react";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  //#region src/Markdown/components/MarkdownProvider.tsx
5
6
  const MarkdownContext = createContext({});
6
7
  const MarkdownProvider = memo(({ children, ...config }) => {
7
8
  return /* @__PURE__ */ jsx(MarkdownContext, {
8
- value: config,
9
+ value: useStableValue(config),
9
10
  children
10
11
  });
11
12
  });
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownProvider.mjs","names":[],"sources":["../../../src/Markdown/components/MarkdownProvider.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, memo, type PropsWithChildren, use } from 'react';\n\nimport { type SyntaxMarkdownProps } from '../type';\n\nexport type MarkdownContentConfig = Omit<SyntaxMarkdownProps, 'children' | 'reactMarkdownProps'>;\n\nexport const MarkdownContext = createContext<MarkdownContentConfig>({});\n\nexport const MarkdownProvider = memo<PropsWithChildren<MarkdownContentConfig>>(\n ({ children, ...config }) => {\n return <MarkdownContext value={config}>{children}</MarkdownContext>;\n },\n);\n\nexport const useMarkdownContext = () => {\n return use(MarkdownContext);\n};\n"],"mappings":";;;;AAQA,MAAa,kBAAkB,cAAqC,EAAE,CAAC;AAEvE,MAAa,mBAAmB,MAC7B,EAAE,UAAU,GAAG,aAAa;AAC3B,QAAO,oBAAC,iBAAD;EAAiB,OAAO;EAAS;EAA2B,CAAA;EAEtE;AAED,MAAa,2BAA2B;AACtC,QAAO,IAAI,gBAAgB"}
1
+ {"version":3,"file":"MarkdownProvider.mjs","names":[],"sources":["../../../src/Markdown/components/MarkdownProvider.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, memo, type PropsWithChildren, use } from 'react';\n\nimport { useStableValue } from '@/hooks/useStableValue';\n\nimport { type SyntaxMarkdownProps } from '../type';\n\nexport type MarkdownContentConfig = Omit<SyntaxMarkdownProps, 'children' | 'reactMarkdownProps'>;\n\nexport const MarkdownContext = createContext<MarkdownContentConfig>({});\n\nexport const MarkdownProvider = memo<PropsWithChildren<MarkdownContentConfig>>(\n ({ children, ...config }) => {\n // The rest-spread builds a fresh object on every render while `children`\n // changes on every streamed chunk, so without stabilisation each chunk\n // swaps the context identity and re-renders every consumer inside every\n // block, bypassing the per-block memo entirely.\n const stableConfig = useStableValue(config);\n\n return <MarkdownContext value={stableConfig}>{children}</MarkdownContext>;\n },\n);\n\nexport const useMarkdownContext = () => {\n return use(MarkdownContext);\n};\n"],"mappings":";;;;;AAUA,MAAa,kBAAkB,cAAqC,EAAE,CAAC;AAEvE,MAAa,mBAAmB,MAC7B,EAAE,UAAU,GAAG,aAAa;AAO3B,QAAO,oBAAC,iBAAD;EAAiB,OAFH,eAAe,OAEO;EAAG;EAA2B,CAAA;EAE5E;AAED,MAAa,2BAA2B;AACtC,QAAO,IAAI,gBAAgB"}
@@ -1,4 +1,4 @@
1
- import { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
1
+ import { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
2
2
  import { Markdown } from "./Markdown.mjs";
3
3
  import { Typography } from "./Typography.mjs";
4
- export { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
4
+ export { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
@@ -1,11 +1,32 @@
1
1
  import { Root } from "../../node_modules/@types/hast/index.mjs";
2
2
 
3
3
  //#region src/Markdown/plugins/rehypeStreamAnimated.d.ts
4
+ interface StreamAnimatedRuntime {
5
+ births: number[];
6
+ /**
7
+ * Write-once per-char render cache, indexed like `births`:
8
+ * `undefined` = char not rendered yet, `null` = born fully revealed,
9
+ * string = inline style frozen at first render.
10
+ * Freezing the style keeps span props referentially stable across the
11
+ * tail block's re-renders, so React never rewrites `animation-delay`
12
+ * on an in-flight fade (a rewrite restarts the CSS animation).
13
+ */
14
+ styles: (string | null | undefined)[];
15
+ }
4
16
  interface StreamAnimatedOptions {
5
17
  births?: number[];
6
18
  fadeDuration?: number;
19
+ /**
20
+ * `'word'` wraps whitespace-delimited runs in one span instead of one
21
+ * span per char. Every concurrent CSS animation keeps the compositor
22
+ * producing frames and fires animationstart/end through React's root
23
+ * event delegation, so animating ~5x fewer nodes is the main CPU lever —
24
+ * char-level remains available for the finer-grained look.
25
+ */
26
+ granularity?: 'char' | 'word';
7
27
  nowMs?: number;
8
28
  revealed?: boolean;
29
+ runtime?: StreamAnimatedRuntime;
9
30
  }
10
31
  declare const rehypeStreamAnimated: (options?: StreamAnimatedOptions) => (tree: Root) => void;
11
32
  //#endregion
@@ -1,5 +1,14 @@
1
1
  import { visit } from "../../node_modules/unist-util-visit/lib/index.mjs";
2
+ import { getNow } from "../../utils/getNow.mjs";
2
3
  //#region src/Markdown/plugins/rehypeStreamAnimated.ts
4
+ const WORD_SEGMENT_RE = /\s+|\S+/g;
5
+ const wordSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
6
+ const segmentWords = (value) => {
7
+ if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];
8
+ const segments = [];
9
+ for (const item of wordSegmenter.segment(value)) segments.push(item.segment);
10
+ return segments;
11
+ };
3
12
  const BLOCK_TAGS = new Set([
4
13
  "p",
5
14
  "h1",
@@ -23,39 +32,66 @@ function hasClass(node, cls) {
23
32
  return false;
24
33
  }
25
34
  const rehypeStreamAnimated = (options = {}) => {
26
- const { births, fadeDuration = 150, nowMs, revealed = false } = options;
27
- const hasBirths = !revealed && Array.isArray(births) && typeof nowMs === "number";
35
+ const { births, fadeDuration = 150, granularity = "char", nowMs, revealed = false, runtime } = options;
36
+ const resolvedRuntime = revealed ? void 0 : runtime ?? (Array.isArray(births) && typeof nowMs === "number" ? {
37
+ births,
38
+ styles: []
39
+ } : void 0);
40
+ const nowOverride = runtime ? void 0 : nowMs;
28
41
  return (tree) => {
29
42
  let globalCharIndex = 0;
43
+ const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);
30
44
  const shouldSkip = (node) => {
31
45
  return SKIP_TAGS.has(node.tagName) || hasClass(node, "katex");
32
46
  };
47
+ const resolveStyle = (index) => {
48
+ const styles = resolvedRuntime.styles;
49
+ const cached = styles[index];
50
+ if (cached !== void 0) return cached;
51
+ const birthTs = resolvedRuntime.births[index];
52
+ let resolved;
53
+ if (birthTs === void 0) resolved = null;
54
+ else {
55
+ const elapsed = now - birthTs;
56
+ resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;
57
+ }
58
+ styles[index] = resolved;
59
+ return resolved;
60
+ };
61
+ const buildSpan = (value, startIndex) => {
62
+ let className = "stream-char";
63
+ let style;
64
+ if (revealed) className = "stream-char stream-char-revealed";
65
+ else if (resolvedRuntime) {
66
+ const resolved = resolveStyle(startIndex);
67
+ if (resolved === null) className = "stream-char stream-char-revealed";
68
+ else style = resolved;
69
+ }
70
+ const properties = { className };
71
+ if (style !== void 0) properties.style = style;
72
+ return {
73
+ children: [{
74
+ type: "text",
75
+ value
76
+ }],
77
+ properties,
78
+ tagName: "span",
79
+ type: "element"
80
+ };
81
+ };
33
82
  const wrapText = (node) => {
34
83
  const newChildren = [];
35
- for (const child of node.children) if (child.type === "text") for (const char of child.value) {
36
- let className = "stream-char";
37
- let delay;
38
- if (revealed) className = "stream-char stream-char-revealed";
39
- else if (hasBirths) {
40
- const birthTs = births[globalCharIndex];
41
- if (birthTs === void 0) className = "stream-char stream-char-revealed";
42
- else {
43
- const elapsed = nowMs - birthTs;
44
- if (elapsed >= fadeDuration) className = "stream-char stream-char-revealed";
45
- else delay = -elapsed;
46
- }
47
- }
48
- const properties = { className };
49
- if (delay !== void 0 && delay !== 0) properties.style = `animation-delay:${delay}ms`;
50
- newChildren.push({
51
- children: [{
52
- type: "text",
53
- value: char
54
- }],
55
- properties,
56
- tagName: "span",
57
- type: "element"
84
+ for (const child of node.children) if (child.type === "text") if (granularity === "word") for (const segment of segmentWords(child.value)) {
85
+ const startIndex = globalCharIndex;
86
+ for (const _char of segment) globalCharIndex++;
87
+ if (segment.trim() === "") newChildren.push({
88
+ type: "text",
89
+ value: segment
58
90
  });
91
+ else newChildren.push(buildSpan(segment, startIndex));
92
+ }
93
+ else for (const char of child.value) {
94
+ newChildren.push(buildSpan(char, globalCharIndex));
59
95
  globalCharIndex++;
60
96
  }
61
97
  else if (child.type === "element") {
@@ -1 +1 @@
1
- {"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n nowMs?: number;\n revealed?: boolean;\n}\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n if (typeof cn === 'string') return cn.includes(cls);\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const { births, fadeDuration = 150, nowMs, revealed = false } = options;\n const hasBirths = !revealed && Array.isArray(births) && typeof nowMs === 'number';\n\n return (tree: Root) => {\n let globalCharIndex = 0;\n\n const shouldSkip = (node: Element): boolean => {\n return SKIP_TAGS.has(node.tagName) || hasClass(node, 'katex');\n };\n\n const wrapText = (node: Element) => {\n const newChildren: ElementContent[] = [];\n for (const child of node.children) {\n if (child.type === 'text') {\n for (const char of child.value) {\n let className = 'stream-char';\n let delay: number | undefined;\n\n if (revealed) {\n className = 'stream-char stream-char-revealed';\n } else if (hasBirths) {\n const birthTs = births![globalCharIndex];\n if (birthTs === undefined) {\n className = 'stream-char stream-char-revealed';\n } else {\n const elapsed = (nowMs as number) - birthTs;\n if (elapsed >= fadeDuration) {\n className = 'stream-char stream-char-revealed';\n } else {\n // Negative delay = already elapsed ms into the fade.\n // Positive delay = not started yet (char born in the future,\n // i.e. staggered within the same commit).\n delay = -elapsed;\n }\n }\n }\n\n const properties: Record<string, any> = { className };\n if (delay !== undefined && delay !== 0) {\n properties.style = `animation-delay:${delay}ms`;\n }\n newChildren.push({\n children: [{ type: 'text', value: char }],\n properties,\n tagName: 'span',\n type: 'element',\n });\n globalCharIndex++;\n }\n } else if (child.type === 'element') {\n if (!shouldSkip(child)) {\n wrapText(child);\n }\n newChildren.push(child);\n } else {\n newChildren.push(child);\n }\n }\n node.children = newChildren;\n };\n\n visit(tree, 'element', ((node: Element) => {\n if (shouldSkip(node)) return 'skip';\n if (BLOCK_TAGS.has(node.tagName)) {\n wrapText(node);\n return 'skip';\n }\n }) as BuildVisitor<Root, 'element'>);\n };\n};\n"],"mappings":";;AAWA,MAAM,aAAa,IAAI,IAAI;CAAC;CAAK;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAK,CAAC;AAC3E,MAAM,YAAY,IAAI,IAAI;CAAC;CAAO;CAAQ;CAAS;CAAM,CAAC;AAE1D,SAAS,SAAS,MAAe,KAAsB;CACrD,MAAM,KAAK,KAAK,YAAY;AAC5B,KAAI,MAAM,QAAQ,GAAG,CAAE,QAAO,GAAG,MAAM,MAAM,OAAO,EAAE,CAAC,SAAS,IAAI,CAAC;AACrE,KAAI,OAAO,OAAO,SAAU,QAAO,GAAG,SAAS,IAAI;AACnD,QAAO;;AAGT,MAAa,wBAAwB,UAAiC,EAAE,KAAK;CAC3E,MAAM,EAAE,QAAQ,eAAe,KAAK,OAAO,WAAW,UAAU;CAChE,MAAM,YAAY,CAAC,YAAY,MAAM,QAAQ,OAAO,IAAI,OAAO,UAAU;AAEzE,SAAQ,SAAe;EACrB,IAAI,kBAAkB;EAEtB,MAAM,cAAc,SAA2B;AAC7C,UAAO,UAAU,IAAI,KAAK,QAAQ,IAAI,SAAS,MAAM,QAAQ;;EAG/D,MAAM,YAAY,SAAkB;GAClC,MAAM,cAAgC,EAAE;AACxC,QAAK,MAAM,SAAS,KAAK,SACvB,KAAI,MAAM,SAAS,OACjB,MAAK,MAAM,QAAQ,MAAM,OAAO;IAC9B,IAAI,YAAY;IAChB,IAAI;AAEJ,QAAI,SACF,aAAY;aACH,WAAW;KACpB,MAAM,UAAU,OAAQ;AACxB,SAAI,YAAY,KAAA,EACd,aAAY;UACP;MACL,MAAM,UAAW,QAAmB;AACpC,UAAI,WAAW,aACb,aAAY;UAKZ,SAAQ,CAAC;;;IAKf,MAAM,aAAkC,EAAE,WAAW;AACrD,QAAI,UAAU,KAAA,KAAa,UAAU,EACnC,YAAW,QAAQ,mBAAmB,MAAM;AAE9C,gBAAY,KAAK;KACf,UAAU,CAAC;MAAE,MAAM;MAAQ,OAAO;MAAM,CAAC;KACzC;KACA,SAAS;KACT,MAAM;KACP,CAAC;AACF;;YAEO,MAAM,SAAS,WAAW;AACnC,QAAI,CAAC,WAAW,MAAM,CACpB,UAAS,MAAM;AAEjB,gBAAY,KAAK,MAAM;SAEvB,aAAY,KAAK,MAAM;AAG3B,QAAK,WAAW;;AAGlB,QAAM,MAAM,aAAa,SAAkB;AACzC,OAAI,WAAW,KAAK,CAAE,QAAO;AAC7B,OAAI,WAAW,IAAI,KAAK,QAAQ,EAAE;AAChC,aAAS,KAAK;AACd,WAAO;;KAEyB"}
1
+ {"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nimport { getNow } from '@/utils/getNow';\n\nexport interface StreamAnimatedRuntime {\n births: number[];\n /**\n * Write-once per-char render cache, indexed like `births`:\n * `undefined` = char not rendered yet, `null` = born fully revealed,\n * string = inline style frozen at first render.\n * Freezing the style keeps span props referentially stable across the\n * tail block's re-renders, so React never rewrites `animation-delay`\n * on an in-flight fade (a rewrite restarts the CSS animation).\n */\n styles: (string | null | undefined)[];\n}\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n /**\n * `'word'` wraps whitespace-delimited runs in one span instead of one\n * span per char. Every concurrent CSS animation keeps the compositor\n * producing frames and fires animationstart/end through React's root\n * event delegation, so animating ~5x fewer nodes is the main CPU lever —\n * char-level remains available for the finer-grained look.\n */\n granularity?: 'char' | 'word';\n nowMs?: number;\n revealed?: boolean;\n runtime?: StreamAnimatedRuntime;\n}\n\n// Intl.Segmenter splits CJK runs into words too — the whitespace regex\n// fallback would otherwise fade an entire unspaced CJK paragraph as one\n// unit.\nconst WORD_SEGMENT_RE = /\\s+|\\S+/g;\n\nconst wordSegmenter =\n typeof Intl !== 'undefined' && 'Segmenter' in Intl\n ? new Intl.Segmenter(undefined, { granularity: 'word' })\n : null;\n\nconst segmentWords = (value: string): string[] => {\n if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];\n\n const segments: string[] = [];\n for (const item of wordSegmenter.segment(value)) {\n segments.push(item.segment);\n }\n return segments;\n};\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n if (typeof cn === 'string') return cn.includes(cls);\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const {\n births,\n fadeDuration = 150,\n granularity = 'char',\n nowMs,\n revealed = false,\n runtime,\n } = options;\n // Legacy births/nowMs callers share the runtime path through a throwaway\n // cache: the plugin factory runs once per render, so their styles are\n // recomputed against the caller's nowMs each run, exactly as before.\n const resolvedRuntime = revealed\n ? undefined\n : (runtime ??\n (Array.isArray(births) && typeof nowMs === 'number' ? { births, styles: [] } : undefined));\n const nowOverride = runtime ? undefined : nowMs;\n\n return (tree: Root) => {\n let globalCharIndex = 0;\n const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);\n\n const shouldSkip = (node: Element): boolean => {\n return SKIP_TAGS.has(node.tagName) || hasClass(node, 'katex');\n };\n\n const resolveStyle = (index: number): string | null => {\n const styles = resolvedRuntime!.styles;\n const cached = styles[index];\n if (cached !== undefined) return cached;\n\n const birthTs = resolvedRuntime!.births[index];\n let resolved: string | null;\n if (birthTs === undefined) {\n resolved = null;\n } else {\n const elapsed = now - birthTs;\n // Negative delay = already elapsed ms into the fade. Positive\n // delay = not started yet (char born in the future, i.e.\n // staggered within the same commit).\n resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;\n }\n styles[index] = resolved;\n return resolved;\n };\n\n const buildSpan = (value: string, startIndex: number): ElementContent => {\n let className = 'stream-char';\n let style: string | undefined;\n\n if (revealed) {\n className = 'stream-char stream-char-revealed';\n } else if (resolvedRuntime) {\n const resolved = resolveStyle(startIndex);\n if (resolved === null) {\n className = 'stream-char stream-char-revealed';\n } else {\n style = resolved;\n }\n }\n\n const properties: Record<string, any> = { className };\n if (style !== undefined) {\n properties.style = style;\n }\n return {\n children: [{ type: 'text', value }],\n properties,\n tagName: 'span',\n type: 'element',\n };\n };\n\n const wrapText = (node: Element) => {\n const newChildren: ElementContent[] = [];\n for (const child of node.children) {\n if (child.type === 'text') {\n if (granularity === 'word') {\n for (const segment of segmentWords(child.value)) {\n const startIndex = globalCharIndex;\n for (const _char of segment) globalCharIndex++;\n\n if (segment.trim() === '') {\n newChildren.push({ type: 'text', value: segment });\n } else {\n newChildren.push(buildSpan(segment, startIndex));\n }\n }\n } else {\n for (const char of child.value) {\n newChildren.push(buildSpan(char, globalCharIndex));\n globalCharIndex++;\n }\n }\n } else if (child.type === 'element') {\n if (!shouldSkip(child)) {\n wrapText(child);\n }\n newChildren.push(child);\n } else {\n newChildren.push(child);\n }\n }\n node.children = newChildren;\n };\n\n visit(tree, 'element', ((node: Element) => {\n if (shouldSkip(node)) return 'skip';\n if (BLOCK_TAGS.has(node.tagName)) {\n wrapText(node);\n return 'skip';\n }\n }) as BuildVisitor<Root, 'element'>);\n };\n};\n"],"mappings":"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"}
@@ -21,6 +21,7 @@ interface TypographyProps extends DivProps {
21
21
  ref?: Ref<HTMLDivElement>;
22
22
  }
23
23
  type StreamSmoothingPreset = 'realtime' | 'balanced' | 'silky';
24
+ type StreamAnimationGranularity = 'char' | 'word';
24
25
  interface SyntaxMarkdownProps {
25
26
  allowHtml?: boolean;
26
27
  allowHtmlList?: ElementType[];
@@ -50,6 +51,7 @@ interface SyntaxMarkdownProps {
50
51
  remarkPlugins?: Pluggable[];
51
52
  remarkPluginsAhead?: Pluggable[];
52
53
  showFootnotes?: boolean;
54
+ streamAnimationGranularity?: StreamAnimationGranularity;
53
55
  streamSmoothingPreset?: StreamSmoothingPreset;
54
56
  variant?: 'default' | 'chat';
55
57
  }
@@ -64,5 +66,5 @@ interface MarkdownProps extends SyntaxMarkdownProps, Omit<TypographyProps, 'chil
64
66
  style?: CSSProperties;
65
67
  }
66
68
  //#endregion
67
- export { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps };
69
+ export { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps };
68
70
  //# sourceMappingURL=type.d.mts.map
package/es/Tabs/Tabs.mjs CHANGED
@@ -8,21 +8,13 @@ import { MoreHorizontalIcon } from "lucide-react";
8
8
  //#region src/Tabs/Tabs.tsx
9
9
  const Tabs$1 = ({ className, compact, variant = "rounded", items, classNames, ...rest }) => {
10
10
  const hasContent = items?.some((item) => !!item.children);
11
- const mergedClassNames = typeof classNames === "function" ? (info) => {
12
- const resolved = classNames(info);
13
- return {
14
- ...resolved,
15
- popup: {
16
- root: styles.dropdown,
17
- ...resolved?.popup
18
- }
19
- };
20
- } : {
11
+ const popupClassNames = {
12
+ root: styles.dropdown,
13
+ ...typeof classNames === "function" ? void 0 : classNames?.popup
14
+ };
15
+ const mergedClassNames = typeof classNames === "function" ? Object.assign((info) => classNames(info), { popup: popupClassNames }) : {
21
16
  ...classNames,
22
- popup: {
23
- root: styles.dropdown,
24
- ...classNames?.popup
25
- }
17
+ popup: popupClassNames
26
18
  };
27
19
  return /* @__PURE__ */ jsx(Tabs, {
28
20
  className: cx(variants({
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.mjs","names":["Tabs","AntdTabs"],"sources":["../../src/Tabs/Tabs.tsx"],"sourcesContent":["'use client';\n\nimport { Tabs as AntdTabs } from 'antd';\nimport { cx } from 'antd-style';\nimport { MoreHorizontalIcon } from 'lucide-react';\nimport { type FC } from 'react';\n\nimport ActionIcon from '@/ActionIcon';\n\nimport { styles, variants } from './style';\nimport type { TabsProps } from './type';\n\nconst Tabs: FC<TabsProps> = ({\n className,\n compact,\n variant = 'rounded',\n items,\n classNames,\n ...rest\n}) => {\n const hasContent = items?.some((item) => !!item.children);\n const mergedClassNames: TabsProps['classNames'] =\n typeof classNames === 'function'\n ? (info) => {\n const resolved = classNames(info);\n\n return {\n ...resolved,\n popup: {\n root: styles.dropdown,\n ...resolved?.popup,\n },\n };\n }\n : {\n ...classNames,\n popup: {\n root: styles.dropdown,\n ...classNames?.popup,\n },\n };\n\n return (\n <AntdTabs\n className={cx(variants({ compact, underlined: hasContent, variant }), className)}\n items={items}\n {...rest}\n classNames={mergedClassNames}\n more={{\n icon: <ActionIcon icon={MoreHorizontalIcon} />,\n ...rest?.more,\n }}\n />\n );\n};\n\nTabs.displayName = 'Tabs';\n\nexport default Tabs;\n"],"mappings":";;;;;;;;AAYA,MAAMA,UAAuB,EAC3B,WACA,SACA,UAAU,WACV,OACA,YACA,GAAG,WACC;CACJ,MAAM,aAAa,OAAO,MAAM,SAAS,CAAC,CAAC,KAAK,SAAS;CACzD,MAAM,mBACJ,OAAO,eAAe,cACjB,SAAS;EACR,MAAM,WAAW,WAAW,KAAK;AAEjC,SAAO;GACL,GAAG;GACH,OAAO;IACL,MAAM,OAAO;IACb,GAAG,UAAU;IACd;GACF;KAEH;EACE,GAAG;EACH,OAAO;GACL,MAAM,OAAO;GACb,GAAG,YAAY;GAChB;EACF;AAEP,QACE,oBAACC,MAAD;EACE,WAAW,GAAG,SAAS;GAAE;GAAS,YAAY;GAAY;GAAS,CAAC,EAAE,UAAU;EACzE;EACP,GAAI;EACJ,YAAY;EACZ,MAAM;GACJ,MAAM,oBAAC,YAAD,EAAY,MAAM,oBAAsB,CAAA;GAC9C,GAAG,MAAM;GACV;EACD,CAAA;;AAIN,OAAK,cAAc"}
1
+ {"version":3,"file":"Tabs.mjs","names":["Tabs","AntdTabs"],"sources":["../../src/Tabs/Tabs.tsx"],"sourcesContent":["'use client';\n\nimport { Tabs as AntdTabs } from 'antd';\nimport { cx } from 'antd-style';\nimport { MoreHorizontalIcon } from 'lucide-react';\nimport { type FC } from 'react';\n\nimport ActionIcon from '@/ActionIcon';\n\nimport { styles, variants } from './style';\nimport type { TabsProps } from './type';\n\nconst Tabs: FC<TabsProps> = ({\n className,\n compact,\n variant = 'rounded',\n items,\n classNames,\n ...rest\n}) => {\n const hasContent = items?.some((item) => !!item.children);\n const popupClassNames = {\n root: styles.dropdown,\n ...(typeof classNames === 'function' ? undefined : classNames?.popup),\n };\n const mergedClassNames: TabsProps['classNames'] =\n typeof classNames === 'function'\n ? Object.assign((info: Parameters<typeof classNames>[0]) => classNames(info), {\n popup: popupClassNames,\n })\n : {\n ...classNames,\n popup: popupClassNames,\n };\n\n return (\n <AntdTabs\n className={cx(variants({ compact, underlined: hasContent, variant }), className)}\n items={items}\n {...rest}\n classNames={mergedClassNames}\n more={{\n icon: <ActionIcon icon={MoreHorizontalIcon} />,\n ...rest?.more,\n }}\n />\n );\n};\n\nTabs.displayName = 'Tabs';\n\nexport default Tabs;\n"],"mappings":";;;;;;;;AAYA,MAAMA,UAAuB,EAC3B,WACA,SACA,UAAU,WACV,OACA,YACA,GAAG,WACC;CACJ,MAAM,aAAa,OAAO,MAAM,SAAS,CAAC,CAAC,KAAK,SAAS;CACzD,MAAM,kBAAkB;EACtB,MAAM,OAAO;EACb,GAAI,OAAO,eAAe,aAAa,KAAA,IAAY,YAAY;EAChE;CACD,MAAM,mBACJ,OAAO,eAAe,aAClB,OAAO,QAAQ,SAA2C,WAAW,KAAK,EAAE,EAC1E,OAAO,iBACR,CAAC,GACF;EACE,GAAG;EACH,OAAO;EACR;AAEP,QACE,oBAACC,MAAD;EACE,WAAW,GAAG,SAAS;GAAE;GAAS,YAAY;GAAY;GAAS,CAAC,EAAE,UAAU;EACzE;EACP,GAAI;EACJ,YAAY;EACZ,MAAM;GACJ,MAAM,oBAAC,YAAD,EAAY,MAAM,oBAAsB,CAAA;GAC9C,GAAG,MAAM;GACV;EACD,CAAA;;AAIN,OAAK,cAAc"}
@@ -0,0 +1,8 @@
1
+ import { TabsProps } from "./type.mjs";
2
+ import { FC } from "react";
3
+
4
+ //#region src/base-ui/Tabs/Tabs.d.ts
5
+ declare const Tabs: FC<TabsProps>;
6
+ //#endregion
7
+ export { Tabs };
8
+ //# sourceMappingURL=Tabs.d.mts.map
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { styles } from "./style.mjs";
3
+ import { TabsIndicator, TabsList, TabsPanel, TabsRoot, TabsTab } from "./atoms.mjs";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { cx } from "antd-style";
6
+ import useMergeState from "use-merge-value";
7
+ //#region src/base-ui/Tabs/Tabs.tsx
8
+ const Tabs = ({ activeKey, className, classNames, defaultActiveKey, items, onChange, orientation = "horizontal", ref, size = "middle", style, styles: customStyles, variant = "rounded" }) => {
9
+ const [value, setValue] = useMergeState(defaultActiveKey ?? null, {
10
+ defaultValue: defaultActiveKey,
11
+ onChange: (next) => {
12
+ if (next != null) onChange?.(next);
13
+ },
14
+ value: activeKey
15
+ });
16
+ const hasPanels = items?.some((item) => item.children != null);
17
+ return /* @__PURE__ */ jsxs(TabsRoot, {
18
+ className: cx(styles.root, classNames?.root, className),
19
+ orientation,
20
+ ref,
21
+ size,
22
+ style: {
23
+ ...style,
24
+ ...customStyles?.root
25
+ },
26
+ value,
27
+ variant,
28
+ onValueChange: (next) => setValue(next ?? null),
29
+ children: [/* @__PURE__ */ jsxs(TabsList, {
30
+ className: cx(classNames?.list),
31
+ style: customStyles?.list,
32
+ children: [/* @__PURE__ */ jsx(TabsIndicator, {
33
+ className: cx(classNames?.indicator),
34
+ style: customStyles?.indicator
35
+ }), items?.map((item) => /* @__PURE__ */ jsxs(TabsTab, {
36
+ className: cx(classNames?.tab),
37
+ disabled: item.disabled,
38
+ style: customStyles?.tab,
39
+ value: item.key,
40
+ children: [item.icon, item.label]
41
+ }, item.key))]
42
+ }), hasPanels && items?.map((item) => /* @__PURE__ */ jsx(TabsPanel, {
43
+ className: cx(classNames?.panel),
44
+ style: customStyles?.panel,
45
+ value: item.key,
46
+ children: item.children
47
+ }, item.key))]
48
+ });
49
+ };
50
+ Tabs.displayName = "Tabs";
51
+ //#endregion
52
+ export { Tabs as default };
53
+
54
+ //# sourceMappingURL=Tabs.mjs.map