dsh-better-display 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +41 -0
  5. package/README.md +41 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/cordis.patch.yml +5 -0
  8. package/cordis.yml +4 -0
  9. package/dshx.yml +5 -0
  10. package/lib/client.js +49923 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/dsh-better-display.js +256 -0
  13. package/lib/types/client/Blocks.d.ts +45 -0
  14. package/lib/types/client/Blocks.d.ts.map +1 -0
  15. package/lib/types/client/Blocks.js +136 -0
  16. package/lib/types/client/Blocks.js.map +1 -0
  17. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  18. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.js +170 -0
  20. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  21. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  22. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.js +15 -0
  24. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  25. package/lib/types/client/LiveFold.d.ts +10 -0
  26. package/lib/types/client/LiveFold.d.ts.map +1 -0
  27. package/lib/types/client/LiveFold.js +39 -0
  28. package/lib/types/client/LiveFold.js.map +1 -0
  29. package/lib/types/client/McpAppFrame.d.ts +32 -0
  30. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  31. package/lib/types/client/McpAppFrame.js +193 -0
  32. package/lib/types/client/McpAppFrame.js.map +1 -0
  33. package/lib/types/client/Reader.d.ts +3 -0
  34. package/lib/types/client/Reader.d.ts.map +1 -0
  35. package/lib/types/client/Reader.js +537 -0
  36. package/lib/types/client/Reader.js.map +1 -0
  37. package/lib/types/client/ReasoningCard.d.ts +11 -0
  38. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  39. package/lib/types/client/ReasoningCard.js +301 -0
  40. package/lib/types/client/ReasoningCard.js.map +1 -0
  41. package/lib/types/client/StickyLane.d.ts +8 -0
  42. package/lib/types/client/StickyLane.d.ts.map +1 -0
  43. package/lib/types/client/StickyLane.js +28 -0
  44. package/lib/types/client/StickyLane.js.map +1 -0
  45. package/lib/types/client/TimelineRail.d.ts +10 -0
  46. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  47. package/lib/types/client/TimelineRail.js +168 -0
  48. package/lib/types/client/TimelineRail.js.map +1 -0
  49. package/lib/types/client/ToolActivity.d.ts +17 -0
  50. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  51. package/lib/types/client/ToolActivity.js +192 -0
  52. package/lib/types/client/ToolActivity.js.map +1 -0
  53. package/lib/types/client/TurnMetrics.d.ts +11 -0
  54. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  55. package/lib/types/client/TurnMetrics.js +44 -0
  56. package/lib/types/client/TurnMetrics.js.map +1 -0
  57. package/lib/types/client/WaitingStatus.d.ts +6 -0
  58. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  59. package/lib/types/client/WaitingStatus.js +19 -0
  60. package/lib/types/client/WaitingStatus.js.map +1 -0
  61. package/lib/types/client/conversation-scroll.d.ts +19 -0
  62. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  63. package/lib/types/client/conversation-scroll.js +25 -0
  64. package/lib/types/client/conversation-scroll.js.map +1 -0
  65. package/lib/types/client/deliverables.d.ts +24 -0
  66. package/lib/types/client/deliverables.d.ts.map +1 -0
  67. package/lib/types/client/deliverables.js +104 -0
  68. package/lib/types/client/deliverables.js.map +1 -0
  69. package/lib/types/client/entry-policy.d.ts +10 -0
  70. package/lib/types/client/entry-policy.d.ts.map +1 -0
  71. package/lib/types/client/entry-policy.js +24 -0
  72. package/lib/types/client/entry-policy.js.map +1 -0
  73. package/lib/types/client/entry.d.ts +4 -0
  74. package/lib/types/client/entry.d.ts.map +1 -0
  75. package/lib/types/client/entry.js +34 -0
  76. package/lib/types/client/entry.js.map +1 -0
  77. package/lib/types/client/fold-choreography.d.ts +28 -0
  78. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  79. package/lib/types/client/fold-choreography.js +31 -0
  80. package/lib/types/client/fold-choreography.js.map +1 -0
  81. package/lib/types/client/index.d.ts +7 -0
  82. package/lib/types/client/index.d.ts.map +1 -0
  83. package/lib/types/client/index.js +153 -0
  84. package/lib/types/client/index.js.map +1 -0
  85. package/lib/types/client/live-turn.d.ts +58 -0
  86. package/lib/types/client/live-turn.d.ts.map +1 -0
  87. package/lib/types/client/live-turn.js +144 -0
  88. package/lib/types/client/live-turn.js.map +1 -0
  89. package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
  90. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  91. package/lib/types/client/markdown/MarkdownText.js +171 -0
  92. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  93. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  94. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  95. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  96. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  97. package/lib/types/client/markdown/incremental.d.ts +69 -0
  98. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  99. package/lib/types/client/markdown/incremental.js +108 -0
  100. package/lib/types/client/markdown/incremental.js.map +1 -0
  101. package/lib/types/client/markdown/katex.d.ts +27 -0
  102. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  103. package/lib/types/client/markdown/katex.js +88 -0
  104. package/lib/types/client/markdown/katex.js.map +1 -0
  105. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  106. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  107. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  108. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  109. package/lib/types/client/markdown/parse.d.ts +25 -0
  110. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  111. package/lib/types/client/markdown/parse.js +42 -0
  112. package/lib/types/client/markdown/parse.js.map +1 -0
  113. package/lib/types/client/markdown/render.d.ts +119 -0
  114. package/lib/types/client/markdown/render.d.ts.map +1 -0
  115. package/lib/types/client/markdown/render.js +435 -0
  116. package/lib/types/client/markdown/render.js.map +1 -0
  117. package/lib/types/client/mcp-app.d.ts +50 -0
  118. package/lib/types/client/mcp-app.d.ts.map +1 -0
  119. package/lib/types/client/mcp-app.js +328 -0
  120. package/lib/types/client/mcp-app.js.map +1 -0
  121. package/lib/types/client/message-chrome.d.ts +19 -0
  122. package/lib/types/client/message-chrome.d.ts.map +1 -0
  123. package/lib/types/client/message-chrome.js +42 -0
  124. package/lib/types/client/message-chrome.js.map +1 -0
  125. package/lib/types/client/motion.d.ts +38 -0
  126. package/lib/types/client/motion.d.ts.map +1 -0
  127. package/lib/types/client/motion.js +367 -0
  128. package/lib/types/client/motion.js.map +1 -0
  129. package/lib/types/client/native/ContextBody.d.ts +130 -0
  130. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  131. package/lib/types/client/native/ContextBody.js +398 -0
  132. package/lib/types/client/native/ContextBody.js.map +1 -0
  133. package/lib/types/client/native/ContextInjectionRow.d.ts +26 -0
  134. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  135. package/lib/types/client/native/ContextInjectionRow.js +27 -0
  136. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  137. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  138. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  139. package/lib/types/client/native/ReferenceIcon.js +16 -0
  140. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  141. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  142. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  143. package/lib/types/client/native/tool-call-model.js +219 -0
  144. package/lib/types/client/native/tool-call-model.js.map +1 -0
  145. package/lib/types/client/open-file.d.ts +24 -0
  146. package/lib/types/client/primitive-labels.d.ts +10 -0
  147. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  148. package/lib/types/client/primitive-labels.js +37 -0
  149. package/lib/types/client/primitive-labels.js.map +1 -0
  150. package/lib/types/client/projection.d.ts +41 -0
  151. package/lib/types/client/projection.d.ts.map +1 -0
  152. package/lib/types/client/projection.js +112 -0
  153. package/lib/types/client/projection.js.map +1 -0
  154. package/lib/types/client/reasoning-follow.d.ts +6 -0
  155. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  156. package/lib/types/client/reasoning-follow.js +12 -0
  157. package/lib/types/client/reasoning-follow.js.map +1 -0
  158. package/lib/types/client/settings-copy.d.ts +6 -0
  159. package/lib/types/client/settings-slots.d.ts +16 -0
  160. package/lib/types/client/store.d.ts +17 -0
  161. package/lib/types/client/store.d.ts.map +1 -0
  162. package/lib/types/client/store.js +13 -0
  163. package/lib/types/client/store.js.map +1 -0
  164. package/lib/types/client/stream-buffer.d.ts +29 -0
  165. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  166. package/lib/types/client/stream-buffer.js +113 -0
  167. package/lib/types/client/stream-buffer.js.map +1 -0
  168. package/lib/types/client/streaming.d.ts +18 -0
  169. package/lib/types/client/streaming.d.ts.map +1 -0
  170. package/lib/types/client/streaming.js +77 -0
  171. package/lib/types/client/streaming.js.map +1 -0
  172. package/lib/types/client/timeline.d.ts +26 -0
  173. package/lib/types/client/timeline.d.ts.map +1 -0
  174. package/lib/types/client/timeline.js +48 -0
  175. package/lib/types/client/timeline.js.map +1 -0
  176. package/lib/types/client/tool-activity.d.ts +52 -0
  177. package/lib/types/client/tool-activity.d.ts.map +1 -0
  178. package/lib/types/client/tool-activity.js +178 -0
  179. package/lib/types/client/tool-activity.js.map +1 -0
  180. package/lib/types/client/types.d.ts +66 -0
  181. package/lib/types/client/types.d.ts.map +1 -0
  182. package/lib/types/client/types.js +2 -0
  183. package/lib/types/client/types.js.map +1 -0
  184. package/lib/types/client/waiting-clock.d.ts +17 -0
  185. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  186. package/lib/types/client/waiting-clock.js +24 -0
  187. package/lib/types/client/waiting-clock.js.map +1 -0
  188. package/lib/types/client/word-motion.d.ts +16 -0
  189. package/lib/types/client/word-motion.d.ts.map +1 -0
  190. package/lib/types/client/word-motion.js +89 -0
  191. package/lib/types/client/word-motion.js.map +1 -0
  192. package/lib/types/client/word-timeline.d.ts +28 -0
  193. package/lib/types/client/word-timeline.d.ts.map +1 -0
  194. package/lib/types/client/word-timeline.js +100 -0
  195. package/lib/types/client/word-timeline.js.map +1 -0
  196. package/lib/types/dsh-better-display.d.ts +17 -0
  197. package/lib/types/dsh-better-display.d.ts.map +1 -0
  198. package/lib/types/dsh-better-display.js +53 -0
  199. package/lib/types/dsh-better-display.js.map +1 -0
  200. package/package.json +121 -0
  201. package/scripts/build-motion-fixture.mjs +26 -0
  202. package/scripts/link-harness-dependencies.mjs +51 -0
  203. package/skills/generative-mcpapps/SKILL.md +47 -0
  204. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  205. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  206. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  207. package/src/client/Blocks.tsx +205 -0
  208. package/src/client/ChoreographedFlow.tsx +182 -0
  209. package/src/client/ClosedProcessSummary.tsx +19 -0
  210. package/src/client/LiveFold.tsx +43 -0
  211. package/src/client/McpAppFrame.module.css +210 -0
  212. package/src/client/McpAppFrame.tsx +317 -0
  213. package/src/client/Reader.module.css +329 -0
  214. package/src/client/Reader.tsx +814 -0
  215. package/src/client/ReasoningCard.tsx +285 -0
  216. package/src/client/SettingsSection.module.css +147 -0
  217. package/src/client/SettingsSection.tsx +117 -0
  218. package/src/client/StickyLane.tsx +24 -0
  219. package/src/client/TimelineRail.module.css +170 -0
  220. package/src/client/TimelineRail.tsx +262 -0
  221. package/src/client/ToolActivity.tsx +214 -0
  222. package/src/client/TurnMetrics.module.css +139 -0
  223. package/src/client/TurnMetrics.tsx +160 -0
  224. package/src/client/WaitingStatus.tsx +22 -0
  225. package/src/client/conversation-scroll.ts +28 -0
  226. package/src/client/deliverables.ts +122 -0
  227. package/src/client/entry-policy.ts +20 -0
  228. package/src/client/entry.tsx +40 -0
  229. package/src/client/fold-choreography.ts +38 -0
  230. package/src/client/index.tsx +198 -0
  231. package/src/client/live-turn.ts +155 -0
  232. package/src/client/markdown/MarkdownText.module.css +304 -0
  233. package/src/client/markdown/MarkdownText.tsx +187 -0
  234. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  235. package/src/client/markdown/incremental.ts +131 -0
  236. package/src/client/markdown/katex.tsx +91 -0
  237. package/src/client/markdown/mathCompatibility.ts +350 -0
  238. package/src/client/markdown/parse.ts +45 -0
  239. package/src/client/markdown/render.tsx +630 -0
  240. package/src/client/markdown/upstream.json +14 -0
  241. package/src/client/mcp-app.ts +323 -0
  242. package/src/client/message-chrome.ts +47 -0
  243. package/src/client/motion.tsx +338 -0
  244. package/src/client/native/ContextBody.module.css +161 -0
  245. package/src/client/native/ContextBody.tsx +591 -0
  246. package/src/client/native/ContextInjectionRow.module.css +62 -0
  247. package/src/client/native/ContextInjectionRow.tsx +72 -0
  248. package/src/client/native/ReferenceIcon.tsx +35 -0
  249. package/src/client/native/tool-call-model.ts +248 -0
  250. package/src/client/native/upstream.json +41 -0
  251. package/src/client/open-file.ts +94 -0
  252. package/src/client/pending-submission.ts +49 -0
  253. package/src/client/primitive-labels.ts +53 -0
  254. package/src/client/projection.ts +118 -0
  255. package/src/client/reasoning-follow.ts +12 -0
  256. package/src/client/settings-copy.ts +53 -0
  257. package/src/client/settings-slots.ts +20 -0
  258. package/src/client/settings.tsx +73 -0
  259. package/src/client/skill-status.ts +78 -0
  260. package/src/client/store.ts +30 -0
  261. package/src/client/stream-buffer.ts +100 -0
  262. package/src/client/streaming.tsx +62 -0
  263. package/src/client/style.d.ts +4 -0
  264. package/src/client/timeline.ts +77 -0
  265. package/src/client/tool-activity.ts +152 -0
  266. package/src/client/types.ts +66 -0
  267. package/src/client/waiting-clock.ts +25 -0
  268. package/src/client/word-motion.tsx +91 -0
  269. package/src/client/word-timeline.ts +96 -0
  270. package/src/dsh-better-display.ts +108 -0
  271. package/src/skill-roots.ts +138 -0
  272. package/src/skill-status.ts +81 -0
  273. package/tests/browser-closed-gap.tsx +20 -0
  274. package/tests/browser-motion.tsx +65 -0
  275. package/tests/browser-sticky-lanes.tsx +22 -0
  276. package/tests/browser-waiting-clock.tsx +14 -0
  277. package/tests/chat-flow-hook.test.ts +66 -0
  278. package/tests/cloud-gate.test.ts +34 -0
  279. package/tests/conversation-scroll.test.ts +47 -0
  280. package/tests/deliverables.test.ts +128 -0
  281. package/tests/entry.test.ts +42 -0
  282. package/tests/fixtures/maid-composer-hide.html +31 -0
  283. package/tests/fold-choreography.test.ts +38 -0
  284. package/tests/history-word-regression.test.ts +16 -0
  285. package/tests/live-turn.test.ts +218 -0
  286. package/tests/mcp-apps.test.ts +157 -0
  287. package/tests/message-chrome.test.ts +29 -0
  288. package/tests/open-file.test.ts +103 -0
  289. package/tests/pending-submission.test.ts +45 -0
  290. package/tests/projection.test.ts +184 -0
  291. package/tests/reasoning-follow.test.ts +34 -0
  292. package/tests/settings-copy.test.ts +63 -0
  293. package/tests/skill-status.test.ts +137 -0
  294. package/tests/stock-install.test.ts +50 -0
  295. package/tests/stream-buffer.test.ts +97 -0
  296. package/tests/timeline.test.ts +59 -0
  297. package/tests/waiting-clock.test.ts +24 -0
  298. package/tests/word-timeline.test.ts +136 -0
  299. package/tsconfig.json +11 -0
  300. package/tsdown.config.ts +49 -0
@@ -0,0 +1,338 @@
1
+ import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import type { ReactNode, RefObject } from 'react';
3
+ import css from './Reader.module.css';
4
+ import { StreamMotionContext } from './streaming.js';
5
+
6
+ const EASING = 'cubic-bezier(.22,1,.36,1)';
7
+
8
+ export function useMotionAllowed(enabled: boolean): boolean {
9
+ const [reduced, setReduced] = useState(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
10
+ useEffect(() => {
11
+ const query = window.matchMedia('(prefers-reduced-motion: reduce)');
12
+ const change = () => setReduced(query.matches);
13
+ query.addEventListener('change', change);
14
+ return () => query.removeEventListener('change', change);
15
+ }, []);
16
+ return enabled && !reduced;
17
+ }
18
+
19
+ export function usePinnedSelection(root: RefObject<HTMLElement>, selector = '[data-reader-answer], [data-reader-process]'): readonly string[] {
20
+ const [keys, setKeys] = useState<readonly string[]>([]);
21
+ useEffect(() => {
22
+ const update = () => {
23
+ const selection = document.getSelection();
24
+ const range = selection && !selection.isCollapsed && selection.rangeCount ? selection.getRangeAt(0) : null;
25
+ const next = range && root.current
26
+ ? [...new Set(Array.from(root.current.querySelectorAll<HTMLElement>(selector))
27
+ .filter(element => range.intersectsNode(element))
28
+ .map(element => element.dataset.readerKey ?? element.dataset.readerProcessKey!).filter(Boolean))]
29
+ : [];
30
+ setKeys(previous => previous.length === next.length && previous.every((key, index) => key === next[index]) ? previous : next);
31
+ };
32
+ document.addEventListener('selectionchange', update);
33
+ return () => document.removeEventListener('selectionchange', update);
34
+ }, [root, selector]);
35
+ return keys;
36
+ }
37
+
38
+ export function StatusText({ text, motion, shimmer = false }: { text: string; motion: boolean; shimmer?: boolean }) {
39
+ const ref = useRef<HTMLSpanElement>(null);
40
+ const [visible, setVisible] = useState(() => !document.hidden);
41
+ const [forcedColors, setForcedColors] = useState(() => window.matchMedia('(forced-colors: active)').matches);
42
+ const allowed = motion && visible && !forcedColors;
43
+ const [frame, setFrame] = useState<{
44
+ text: string; id: number; phase: 'idle' | 'start' | 'running';
45
+ outgoing: { text: string; id: number } | null;
46
+ }>({ text, id: 0, phase: 'idle', outgoing: null });
47
+ // Adjust before commit, so a new label cannot paint once before its entry state.
48
+ // Reuse the previous incoming key: a rapid change exits from its current pose.
49
+ if (frame.text !== text) setFrame({
50
+ text, id: frame.id + 1, phase: allowed ? 'start' : 'idle',
51
+ outgoing: allowed ? { text: frame.text, id: frame.id } : null,
52
+ });
53
+ useEffect(() => {
54
+ const update = () => setVisible(!document.hidden);
55
+ const query = window.matchMedia('(forced-colors: active)');
56
+ const colors = () => setForcedColors(query.matches);
57
+ document.addEventListener('visibilitychange', update);
58
+ query.addEventListener('change', colors);
59
+ return () => { document.removeEventListener('visibilitychange', update); query.removeEventListener('change', colors); };
60
+ }, []);
61
+ useLayoutEffect(() => {
62
+ const id = frame.id;
63
+ const settle = () => setFrame(current => current.id === id && current.outgoing
64
+ ? { ...current, phase: 'idle', outgoing: null } : current);
65
+ if (!allowed) { settle(); return; }
66
+ if (!frame.outgoing || !ref.current) return;
67
+ // Commit the supplied .is-enter-start pose before releasing CSS transitions.
68
+ ref.current.getBoundingClientRect();
69
+ let timer = 0;
70
+ const tick = requestAnimationFrame(() => {
71
+ setFrame(current => current.id === id ? { ...current, phase: 'running' } : current);
72
+ timer = window.setTimeout(settle, 200); // 150ms swap + 50ms incoming gap.
73
+ });
74
+ return () => { cancelAnimationFrame(tick); window.clearTimeout(timer); };
75
+ }, [frame.id, allowed]);
76
+ const active = shimmer && allowed;
77
+ const swapping = allowed && frame.outgoing !== null;
78
+ return <span className={css.statusText} data-reader-status data-reader-busy={shimmer} data-ud-check="reader-status">
79
+ <span className={css.think} aria-hidden="true" data-active={active} data-reader-status-phase={swapping ? frame.phase : 'idle'} data-ud-motion="reader-thinking-state">
80
+ <span className={css.thinkSizer}>{text}</span>
81
+ {swapping && <span key={frame.outgoing!.id} className={`${css.thinkText} ${frame.phase === 'running' ? css.isExit : ''}`}
82
+ data-reader-status-copy="outgoing" data-text={frame.outgoing!.text}>{frame.outgoing!.text}</span>}
83
+ <span key={frame.id} ref={ref} className={`${css.thinkText} ${swapping && frame.phase === 'start' ? css.isEnterStart : ''}`}
84
+ data-reader-status-copy="current" data-reader-shimmer={active || undefined} data-text={text}>{text}</span>
85
+ </span>
86
+ <span className={css.srOnly} role="status" aria-live="polite" aria-atomic="true">{text}</span>
87
+ </span>;
88
+ }
89
+
90
+ export function Disclosure({ open, onChange, label, controls, buttonRef, ariaLabel }: {
91
+ open: boolean; onChange: (value: boolean) => void; label: ReactNode;
92
+ controls?: string; buttonRef: RefObject<HTMLButtonElement>;
93
+ ariaLabel?: string;
94
+ }) {
95
+ const name = ariaLabel ?? '思考与过程';
96
+ return <div className={css.disclosure} data-reader-disclosure data-expanded={open}>
97
+ <button ref={buttonRef} type="button" className={css.disclosureButton} aria-label={`${open ? '收起' : '展开'}${name}`} aria-expanded={open} {...(controls ? { 'aria-controls': controls } : {})} onClick={() => onChange(!open)}>
98
+ {label}
99
+ <svg className={css.chevron} data-open={open} viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="m6 4 4 4-4 4" /></svg>
100
+ </button>
101
+ </div>;
102
+ }
103
+
104
+ /** Supplemental details stay in source order beside their own narration. */
105
+ export function ProcessFragment({ open, motion, onRead, returnFocusTo, nodeKey, children, framed = false }: {
106
+ open: boolean; motion: boolean; onRead: () => void; nodeKey: string;
107
+ returnFocusTo: RefObject<HTMLElement>; children: ReactNode; framed?: boolean;
108
+ }) {
109
+ const body = useRef<HTMLDivElement>(null);
110
+ const running = useRef<Animation | null>(null);
111
+ const previous = useRef(open);
112
+ const [present, setPresent] = useState(open);
113
+ useLayoutEffect(() => {
114
+ const element = body.current;
115
+ if (!element) return;
116
+ const from = running.current ? element.getBoundingClientRect().height : previous.current ? element.scrollHeight : 0;
117
+ running.current?.cancel();
118
+ running.current = null;
119
+ const changed = previous.current !== open;
120
+ previous.current = open;
121
+ if (open) setPresent(true);
122
+ if (!open && element.contains(document.activeElement)) returnFocusTo.current?.focus();
123
+ element.style.height = open ? 'auto' : '0px';
124
+ const target = open ? element.scrollHeight : 0;
125
+ if (!motion || !changed || Math.abs(from - target) < 1) {
126
+ setPresent(open);
127
+ return;
128
+ }
129
+ if (from > target && motion) {
130
+ document.body.dataset.readerFolding = 'true';
131
+ }
132
+ const animation = element.animate(
133
+ from > target
134
+ ? [
135
+ { offset: 0, height: `${from}px`, opacity: 1, transform: 'scaleY(1) translateY(0)' },
136
+ { offset: 0.32, height: `${Math.round(from * 0.92)}px`, opacity: 0.35, transform: 'scaleY(0.96) translateY(-2px)' },
137
+ { offset: 1, height: `${target}px`, opacity: 0, transform: 'scaleY(0.68) translateY(-8px)' },
138
+ ]
139
+ : [
140
+ { offset: 0, height: `${from}px`, opacity: 0, transform: 'scaleY(0.8) translateY(6px)' },
141
+ { offset: 0.3, height: `${Math.round(target * 0.4)}px`, opacity: 0.4, transform: 'scaleY(0.92) translateY(3px)' },
142
+ { offset: 1, height: `${target}px`, opacity: 1, transform: 'scaleY(1) translateY(0)' },
143
+ ],
144
+ { duration: 380, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'both' },
145
+ );
146
+ running.current = animation;
147
+ animation.onfinish = () => {
148
+ if (running.current !== animation) return;
149
+ delete document.body.dataset.readerFolding;
150
+ running.current = null;
151
+ animation.cancel();
152
+ setPresent(open);
153
+ };
154
+ }, [open, motion, returnFocusTo]);
155
+ useEffect(() => () => { delete document.body.dataset.readerFolding; running.current?.cancel(); }, []);
156
+ if (!open && !present) return null;
157
+ return <div ref={body} className={css.disclosureBody} data-reader-process data-reader-process-key={nodeKey} data-ud-motion="reader-process-size"
158
+ aria-hidden={!open} onPointerDown={() => { if (open) onRead(); }} onFocusCapture={() => { if (open) onRead(); }} {...(!open ? { inert: '' } : {})}>
159
+ <div className={framed ? css.processFrame : css.processContents}>{children}</div>
160
+ </div>;
161
+ }
162
+
163
+ /** Retire only narration that was actually visible; historical rows stay folded. */
164
+ export function RetiringContent({ visible, children }: { visible: boolean; children: ReactNode }) {
165
+ const { enabled } = useContext(StreamMotionContext);
166
+ const root = useRef<HTMLDivElement>(null);
167
+ const animation = useRef<Animation | null>(null);
168
+ const [present, setPresent] = useState(visible);
169
+ const [focusHeld, setFocusHeld] = useState(false);
170
+ useLayoutEffect(() => {
171
+ const element = root.current;
172
+ if (visible) {
173
+ animation.current?.cancel(); animation.current = null;
174
+ setPresent(true);
175
+ return;
176
+ }
177
+ if (!element) return;
178
+ if (element.contains(document.activeElement)) { setFocusHeld(true); return; }
179
+ if (focusHeld) return;
180
+ const from = element.getBoundingClientRect().height;
181
+ animation.current?.cancel(); animation.current = null;
182
+ if (!enabled || from < 1) { setPresent(false); return; }
183
+ const next = element.animate([{ height: `${from}px`, opacity: 1 }, { height: '0px', opacity: 0 }], { duration: 220, easing: EASING, fill: 'both' });
184
+ animation.current = next;
185
+ next.onfinish = () => { if (animation.current === next) { animation.current = null; next.cancel(); setPresent(false); } };
186
+ }, [visible, enabled, focusHeld]);
187
+ useEffect(() => () => animation.current?.cancel(), []);
188
+ if (!visible && !present) return null;
189
+ return <div ref={root} className={css.retiringContent} data-reader-retiring={visible ? 'visible' : 'retiring'} data-ud-motion="reader-progress-retire"
190
+ onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) setFocusHeld(false); }}>
191
+ {children}
192
+ </div>;
193
+ }
194
+
195
+ // DOM-only behavior: the native Session remains the sole source of business data.
196
+ export function useReadingScroll(root: RefObject<HTMLElement>, motion: boolean): {
197
+ detached: boolean;
198
+ jump: () => void;
199
+ /** Stop tail-follow so a rail landing is not pulled back to the live bottom. */
200
+ release: () => void;
201
+ } {
202
+ const port = useRef<HTMLElement | null>(null);
203
+ const following = useRef(true);
204
+ const anchor = useRef<{ element: HTMLElement; top: number } | null>(null);
205
+ const cancelFollow = useRef<() => void>(() => {});
206
+ const [detached, setDetached] = useState(false);
207
+ useLayoutEffect(() => {
208
+ const content = root.current;
209
+ if (!content) return;
210
+ const scroll = content.closest<HTMLElement>('[data-conversation-scroll]') ?? content;
211
+ port.current = scroll;
212
+ let followFrame = 0;
213
+ let lastFrameAt = 0;
214
+ let layoutDepth = 0;
215
+ let lastWrittenTop: number | null = null;
216
+ const layoutStart = () => {
217
+ layoutDepth++;
218
+ cancelAnimationFrame(followFrame); followFrame = 0;
219
+ capture();
220
+ };
221
+ const layoutEnd = () => {
222
+ layoutDepth = Math.max(0, layoutDepth - 1);
223
+ if (!layoutDepth && following.current && !followFrame) {
224
+ lastFrameAt = performance.now(); followFrame = requestAnimationFrame(follow);
225
+ }
226
+ };
227
+ cancelFollow.current = () => {
228
+ cancelAnimationFrame(followFrame);
229
+ followFrame = 0;
230
+ lastWrittenTop = null;
231
+ };
232
+ const selected = () => {
233
+ const selection = document.getSelection();
234
+ return selection && !selection.isCollapsed && selection.anchorNode && content.contains(selection.anchorNode);
235
+ };
236
+ const capture = () => {
237
+ const top = scroll.getBoundingClientRect().top;
238
+ const candidates = content.querySelectorAll<HTMLElement>('[data-reader-anchor]');
239
+ let candidate: HTMLElement | null = null;
240
+ for (let index = 0; index < candidates.length; index++) {
241
+ const element = candidates[index];
242
+ if (element.getBoundingClientRect().bottom > top + 8) { candidate = element; break; }
243
+ }
244
+ anchor.current = candidate ? { element: candidate, top: candidate.getBoundingClientRect().top } : null;
245
+ };
246
+ // Anchor capture is only consumed while detached; coalesce DOM scans to one per frame.
247
+ let captureFrame = 0;
248
+ const scheduleCapture = () => {
249
+ if (captureFrame) return;
250
+ captureFrame = requestAnimationFrame(() => { captureFrame = 0; capture(); });
251
+ };
252
+ const onScroll = () => {
253
+ // While a follow animation is actively driving scroll, do not cancel following midway.
254
+ if (followFrame !== 0 || layoutDepth > 0) return;
255
+ // Our easing frames must not be mistaken for a user leaving the bottom.
256
+ if (lastWrittenTop !== null && Math.abs(scroll.scrollTop - lastWrittenTop) < 1) return;
257
+ const atBottom = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 25;
258
+ following.current = atBottom;
259
+ setDetached(!atBottom);
260
+ if (!atBottom) { cancelAnimationFrame(followFrame); followFrame = 0; scheduleCapture(); }
261
+ };
262
+ const onWheel = (event: WheelEvent) => {
263
+ cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
264
+ if (event.deltaY < 0) { following.current = false; setDetached(true); scheduleCapture(); }
265
+ };
266
+ const onTouch = () => {
267
+ cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
268
+ };
269
+ const onKey = (event: KeyboardEvent) => {
270
+ if (event.target instanceof HTMLElement && event.target.closest('textarea,input,[contenteditable=true]')) return;
271
+ if (['PageUp', 'Home', 'ArrowUp'].includes(event.key)) {
272
+ cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
273
+ following.current = false; setDetached(true); scheduleCapture();
274
+ }
275
+ };
276
+ const writeTop = (top: number) => { scroll.scrollTop = top; lastWrittenTop = scroll.scrollTop; };
277
+ const follow = (now: number) => {
278
+ followFrame = 0;
279
+ if (!following.current || selected() || content.contains(document.activeElement)) return;
280
+ if (layoutDepth > 0) return;
281
+ const gap = scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop;
282
+ const delta = Math.min(48, Math.max(1, now - lastFrameAt));
283
+ lastFrameAt = now;
284
+ if (!motion || Math.abs(gap) < 1.5) { writeTop(scroll.scrollHeight); capture(); return; }
285
+ writeTop(scroll.scrollTop + gap * (1 - Math.exp(-delta / 52)));
286
+ followFrame = requestAnimationFrame(follow);
287
+ };
288
+ const firstFrame = requestAnimationFrame(() => {
289
+ if (following.current && !selected()) writeTop(scroll.scrollHeight);
290
+ capture();
291
+ });
292
+ const observer = new ResizeObserver(() => {
293
+ if (selected()) return;
294
+ if (following.current && !content.contains(document.activeElement)) {
295
+ // Height is already animated: no second, lagging scroll easing.
296
+ if (!motion || layoutDepth > 0) writeTop(scroll.scrollHeight);
297
+ else if (!followFrame) { lastFrameAt = performance.now(); followFrame = requestAnimationFrame(follow); }
298
+ } else if (!following.current && anchor.current?.element.isConnected) {
299
+ const delta = anchor.current.element.getBoundingClientRect().top - anchor.current.top;
300
+ if (Math.abs(delta) > .5) writeTop(scroll.scrollTop + delta);
301
+ }
302
+ capture();
303
+ });
304
+ observer.observe(content);
305
+ if (scroll !== content) observer.observe(scroll);
306
+ content.addEventListener('reader-layout-start', layoutStart);
307
+ content.addEventListener('reader-layout-end', layoutEnd);
308
+ scroll.addEventListener('scroll', onScroll, { passive: true });
309
+ scroll.addEventListener('wheel', onWheel, { passive: true });
310
+ scroll.addEventListener('touchstart', onTouch, { passive: true });
311
+ scroll.addEventListener('touchmove', onTouch, { passive: true });
312
+ scroll.addEventListener('keydown', onKey);
313
+ return () => {
314
+ cancelAnimationFrame(firstFrame); cancelAnimationFrame(followFrame); cancelAnimationFrame(captureFrame); observer.disconnect();
315
+ content.removeEventListener('reader-layout-start', layoutStart);
316
+ content.removeEventListener('reader-layout-end', layoutEnd);
317
+ scroll.removeEventListener('scroll', onScroll); scroll.removeEventListener('wheel', onWheel);
318
+ scroll.removeEventListener('touchstart', onTouch); scroll.removeEventListener('touchmove', onTouch);
319
+ scroll.removeEventListener('keydown', onKey);
320
+ };
321
+ }, [root, motion]);
322
+ const jump = useCallback(() => {
323
+ cancelFollow.current();
324
+ anchor.current = null;
325
+ following.current = true;
326
+ setDetached(false);
327
+ if (port.current) {
328
+ port.current.scrollTop = port.current.scrollHeight;
329
+ }
330
+ }, []);
331
+ const release = useCallback(() => {
332
+ cancelFollow.current();
333
+ following.current = false;
334
+ anchor.current = null;
335
+ setDetached(true);
336
+ }, []);
337
+ return { detached, jump, release };
338
+ }
@@ -0,0 +1,161 @@
1
+ /* Expanded context bodies: one code-block surface shared by every form, so the
2
+ disclosure keeps the Figma 10:2482 geometry whichever form renders inside. */
3
+
4
+ .text {
5
+ margin: 0;
6
+ color: var(--dsw-alias-label-secondary);
7
+ font: inherit;
8
+ white-space: pre-wrap;
9
+ overflow-wrap: anywhere;
10
+ }
11
+
12
+ /* Source fields beneath the text: dimmer than the content they describe. */
13
+ .fields {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 2px;
17
+ margin: 8px 0 0;
18
+ padding-top: 8px;
19
+ border-top: 1px solid var(--dsw-alias-line-secondary);
20
+ }
21
+
22
+ .field {
23
+ display: flex;
24
+ gap: 8px;
25
+ min-width: 0;
26
+ }
27
+
28
+ .fieldKey {
29
+ flex: none;
30
+ min-width: 96px;
31
+ color: var(--dsw-alias-label-caption);
32
+ }
33
+
34
+ .fieldValue {
35
+ flex: 1 1 auto;
36
+ min-width: 0;
37
+ margin: 0;
38
+ color: var(--dsw-alias-label-tertiary);
39
+ overflow-wrap: anywhere;
40
+ }
41
+
42
+ /* instructions: the reconciled files, above their text. */
43
+ .files {
44
+ display: flex;
45
+ flex-wrap: wrap;
46
+ gap: 4px 12px;
47
+ margin: 0 0 8px;
48
+ padding: 0;
49
+ list-style: none;
50
+ }
51
+
52
+ .file {
53
+ display: flex;
54
+ align-items: baseline;
55
+ gap: 6px;
56
+ min-width: 0;
57
+ }
58
+
59
+ .filePath {
60
+ color: var(--dsw-alias-label-secondary);
61
+ overflow-wrap: anywhere;
62
+ }
63
+
64
+ .fileAction {
65
+ color: var(--dsw-alias-label-caption);
66
+ }
67
+
68
+ /* catalog: a replacement notice above one row per published entry. */
69
+ .catalogNotice {
70
+ margin: 0 0 6px;
71
+ color: var(--dsw-alias-label-caption);
72
+ }
73
+
74
+
75
+ .entries {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 4px;
79
+ margin: 0;
80
+ padding: 0;
81
+ list-style: none;
82
+ }
83
+
84
+ .entry {
85
+ display: flex;
86
+ gap: 8px;
87
+ min-width: 0;
88
+ }
89
+
90
+ .entryName {
91
+ flex: none;
92
+ color: var(--dsw-alias-label-secondary);
93
+ }
94
+
95
+ .entryDescription {
96
+ flex: 1 1 auto;
97
+ min-width: 0;
98
+ overflow: hidden;
99
+ color: var(--dsw-alias-label-tertiary);
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ }
103
+
104
+ /* snapshot: one titled block per contributing subsystem. */
105
+ .sections {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 8px;
109
+ margin: 0;
110
+ }
111
+
112
+ .section {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 2px;
116
+ min-width: 0;
117
+ }
118
+
119
+ .sectionName {
120
+ color: var(--dsw-alias-label-caption);
121
+ }
122
+
123
+ .sectionText {
124
+ margin: 0;
125
+ color: var(--dsw-alias-label-secondary);
126
+ white-space: pre-wrap;
127
+ overflow-wrap: anywhere;
128
+ }
129
+
130
+ /* relay: who sent this, above what they said. */
131
+ .relaySender {
132
+ margin: 0 0 6px;
133
+ color: var(--dsw-alias-label-caption);
134
+ overflow-wrap: anywhere;
135
+ }
136
+
137
+ /* recall: one row per source session, with how much of it survived. */
138
+ .recalls {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 2px;
142
+ margin: 0 0 8px;
143
+ padding: 0;
144
+ list-style: none;
145
+ }
146
+
147
+ .recall {
148
+ display: flex;
149
+ gap: 8px;
150
+ min-width: 0;
151
+ }
152
+
153
+ .recallLabel {
154
+ color: var(--dsw-alias-label-secondary);
155
+ overflow-wrap: anywhere;
156
+ }
157
+
158
+ .recallCounts {
159
+ flex: none;
160
+ color: var(--dsw-alias-label-caption);
161
+ }