@drscrewdriver/dsh-better-display 0.3.3-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +259 -0
- package/DESIGN.md +72 -0
- package/LICENSE +24 -0
- package/README.en.md +55 -0
- package/README.md +55 -0
- package/THIRD_PARTY_NOTICES.md +65 -0
- package/compat/harness-rc2.json +99 -0
- package/cordis.patch.yml +5 -0
- package/cordis.yml +4 -0
- package/docs/official-rendering-bridge.md +65 -0
- package/dshx.yml +5 -0
- package/lib/client.js +49292 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-better-display.js +101 -0
- package/lib/types/client/Blocks.d.ts +49 -0
- package/lib/types/client/Blocks.d.ts.map +1 -0
- package/lib/types/client/Blocks.js +143 -0
- package/lib/types/client/Blocks.js.map +1 -0
- package/lib/types/client/ChoreographedFlow.d.ts +22 -0
- package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
- package/lib/types/client/ChoreographedFlow.js +218 -0
- package/lib/types/client/ChoreographedFlow.js.map +1 -0
- package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
- package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
- package/lib/types/client/ClosedProcessSummary.js +16 -0
- package/lib/types/client/ClosedProcessSummary.js.map +1 -0
- package/lib/types/client/DiffPanel.d.ts +13 -0
- package/lib/types/client/DiffPanel.d.ts.map +1 -0
- package/lib/types/client/DiffPanel.js +115 -0
- package/lib/types/client/DiffPanel.js.map +1 -0
- package/lib/types/client/LiveFold.d.ts +10 -0
- package/lib/types/client/LiveFold.d.ts.map +1 -0
- package/lib/types/client/LiveFold.js +39 -0
- package/lib/types/client/LiveFold.js.map +1 -0
- package/lib/types/client/McpAppFrame.d.ts +32 -0
- package/lib/types/client/McpAppFrame.d.ts.map +1 -0
- package/lib/types/client/McpAppFrame.js +193 -0
- package/lib/types/client/McpAppFrame.js.map +1 -0
- package/lib/types/client/OfficialContent.d.ts +25 -0
- package/lib/types/client/OfficialContent.d.ts.map +1 -0
- package/lib/types/client/OfficialContent.js +53 -0
- package/lib/types/client/OfficialContent.js.map +1 -0
- package/lib/types/client/Reader.d.ts +3 -0
- package/lib/types/client/Reader.d.ts.map +1 -0
- package/lib/types/client/Reader.js +603 -0
- package/lib/types/client/Reader.js.map +1 -0
- package/lib/types/client/ReasoningCard.d.ts +11 -0
- package/lib/types/client/ReasoningCard.d.ts.map +1 -0
- package/lib/types/client/ReasoningCard.js +322 -0
- package/lib/types/client/ReasoningCard.js.map +1 -0
- package/lib/types/client/SettingsSection.d.ts +37 -0
- package/lib/types/client/SettingsSection.d.ts.map +1 -0
- package/lib/types/client/SettingsSection.js +63 -0
- package/lib/types/client/SettingsSection.js.map +1 -0
- package/lib/types/client/StickyLane.d.ts +8 -0
- package/lib/types/client/StickyLane.d.ts.map +1 -0
- package/lib/types/client/StickyLane.js +39 -0
- package/lib/types/client/StickyLane.js.map +1 -0
- package/lib/types/client/TimelineRail.d.ts +10 -0
- package/lib/types/client/TimelineRail.d.ts.map +1 -0
- package/lib/types/client/TimelineRail.js +168 -0
- package/lib/types/client/TimelineRail.js.map +1 -0
- package/lib/types/client/ToolActivity.d.ts +17 -0
- package/lib/types/client/ToolActivity.d.ts.map +1 -0
- package/lib/types/client/ToolActivity.js +244 -0
- package/lib/types/client/ToolActivity.js.map +1 -0
- package/lib/types/client/TurnMetrics.d.ts +11 -0
- package/lib/types/client/TurnMetrics.d.ts.map +1 -0
- package/lib/types/client/TurnMetrics.js +44 -0
- package/lib/types/client/TurnMetrics.js.map +1 -0
- package/lib/types/client/WaitClock.d.ts +16 -0
- package/lib/types/client/WaitClock.d.ts.map +1 -0
- package/lib/types/client/WaitClock.js +29 -0
- package/lib/types/client/WaitClock.js.map +1 -0
- package/lib/types/client/WaitingClock.d.ts +15 -0
- package/lib/types/client/WaitingClock.d.ts.map +1 -0
- package/lib/types/client/WaitingClock.js +30 -0
- package/lib/types/client/WaitingClock.js.map +1 -0
- package/lib/types/client/WaitingStatus.d.ts +6 -0
- package/lib/types/client/WaitingStatus.d.ts.map +1 -0
- package/lib/types/client/WaitingStatus.js +9 -0
- package/lib/types/client/WaitingStatus.js.map +1 -0
- package/lib/types/client/conversation-scroll.d.ts +19 -0
- package/lib/types/client/conversation-scroll.d.ts.map +1 -0
- package/lib/types/client/conversation-scroll.js +25 -0
- package/lib/types/client/conversation-scroll.js.map +1 -0
- package/lib/types/client/deliverables.d.ts +24 -0
- package/lib/types/client/deliverables.d.ts.map +1 -0
- package/lib/types/client/deliverables.js +104 -0
- package/lib/types/client/deliverables.js.map +1 -0
- package/lib/types/client/entry-policy.d.ts +10 -0
- package/lib/types/client/entry-policy.d.ts.map +1 -0
- package/lib/types/client/entry-policy.js +24 -0
- package/lib/types/client/entry-policy.js.map +1 -0
- package/lib/types/client/entry.d.ts +4 -0
- package/lib/types/client/entry.d.ts.map +1 -0
- package/lib/types/client/entry.js +34 -0
- package/lib/types/client/entry.js.map +1 -0
- package/lib/types/client/fold-choreography.d.ts +40 -0
- package/lib/types/client/fold-choreography.d.ts.map +1 -0
- package/lib/types/client/fold-choreography.js +48 -0
- package/lib/types/client/fold-choreography.js.map +1 -0
- package/lib/types/client/fold-intensity.d.ts +11 -0
- package/lib/types/client/fold-intensity.d.ts.map +1 -0
- package/lib/types/client/fold-intensity.js +32 -0
- package/lib/types/client/fold-intensity.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +195 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/live-turn.d.ts +58 -0
- package/lib/types/client/live-turn.d.ts.map +1 -0
- package/lib/types/client/live-turn.js +190 -0
- package/lib/types/client/live-turn.js.map +1 -0
- package/lib/types/client/markdown/MarkdownText.d.ts +41 -0
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
- package/lib/types/client/markdown/MarkdownText.js +178 -0
- package/lib/types/client/markdown/MarkdownText.js.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
- package/lib/types/client/markdown/incremental.d.ts +69 -0
- package/lib/types/client/markdown/incremental.d.ts.map +1 -0
- package/lib/types/client/markdown/incremental.js +108 -0
- package/lib/types/client/markdown/incremental.js.map +1 -0
- package/lib/types/client/markdown/katex.d.ts +27 -0
- package/lib/types/client/markdown/katex.d.ts.map +1 -0
- package/lib/types/client/markdown/katex.js +88 -0
- package/lib/types/client/markdown/katex.js.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.js +293 -0
- package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
- package/lib/types/client/markdown/parse.d.ts +25 -0
- package/lib/types/client/markdown/parse.d.ts.map +1 -0
- package/lib/types/client/markdown/parse.js +42 -0
- package/lib/types/client/markdown/parse.js.map +1 -0
- package/lib/types/client/markdown/render.d.ts +121 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -0
- package/lib/types/client/markdown/render.js +452 -0
- package/lib/types/client/markdown/render.js.map +1 -0
- package/lib/types/client/mcp-app.d.ts +50 -0
- package/lib/types/client/mcp-app.d.ts.map +1 -0
- package/lib/types/client/mcp-app.js +328 -0
- package/lib/types/client/mcp-app.js.map +1 -0
- package/lib/types/client/message-chrome.d.ts +19 -0
- package/lib/types/client/message-chrome.d.ts.map +1 -0
- package/lib/types/client/message-chrome.js +42 -0
- package/lib/types/client/message-chrome.js.map +1 -0
- package/lib/types/client/motion.d.ts +38 -0
- package/lib/types/client/motion.d.ts.map +1 -0
- package/lib/types/client/motion.js +453 -0
- package/lib/types/client/motion.js.map +1 -0
- package/lib/types/client/native/ContextBody.d.ts +130 -0
- package/lib/types/client/native/ContextBody.d.ts.map +1 -0
- package/lib/types/client/native/ContextBody.js +398 -0
- package/lib/types/client/native/ContextBody.js.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts +35 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.js +42 -0
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
- package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
- package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
- package/lib/types/client/native/ReferenceIcon.js +16 -0
- package/lib/types/client/native/ReferenceIcon.js.map +1 -0
- package/lib/types/client/native/tool-call-model.d.ts +58 -0
- package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
- package/lib/types/client/native/tool-call-model.js +219 -0
- package/lib/types/client/native/tool-call-model.js.map +1 -0
- package/lib/types/client/native/tool-update.d.ts +23 -0
- package/lib/types/client/native/tool-update.d.ts.map +1 -0
- package/lib/types/client/native/tool-update.js +22 -0
- package/lib/types/client/native/tool-update.js.map +1 -0
- package/lib/types/client/official-slots.d.ts +71 -0
- package/lib/types/client/official-slots.d.ts.map +1 -0
- package/lib/types/client/official-slots.js +183 -0
- package/lib/types/client/official-slots.js.map +1 -0
- package/lib/types/client/open-file.d.ts +29 -0
- package/lib/types/client/open-file.d.ts.map +1 -0
- package/lib/types/client/open-file.js +71 -0
- package/lib/types/client/open-file.js.map +1 -0
- package/lib/types/client/pending-submission.d.ts +28 -0
- package/lib/types/client/pending-submission.d.ts.map +1 -0
- package/lib/types/client/pending-submission.js +28 -0
- package/lib/types/client/pending-submission.js.map +1 -0
- package/lib/types/client/platform-media.d.ts +6 -0
- package/lib/types/client/platform-media.d.ts.map +1 -0
- package/lib/types/client/platform-media.js +18 -0
- package/lib/types/client/platform-media.js.map +1 -0
- package/lib/types/client/primitive-labels.d.ts +10 -0
- package/lib/types/client/primitive-labels.d.ts.map +1 -0
- package/lib/types/client/primitive-labels.js +38 -0
- package/lib/types/client/primitive-labels.js.map +1 -0
- package/lib/types/client/projection.d.ts +41 -0
- package/lib/types/client/projection.d.ts.map +1 -0
- package/lib/types/client/projection.js +112 -0
- package/lib/types/client/projection.js.map +1 -0
- package/lib/types/client/reasoning-follow.d.ts +10 -0
- package/lib/types/client/reasoning-follow.d.ts.map +1 -0
- package/lib/types/client/reasoning-follow.js +21 -0
- package/lib/types/client/reasoning-follow.js.map +1 -0
- package/lib/types/client/resource-address.d.ts +3 -0
- package/lib/types/client/resource-address.d.ts.map +1 -0
- package/lib/types/client/resource-address.js +33 -0
- package/lib/types/client/resource-address.js.map +1 -0
- package/lib/types/client/settings-copy.d.ts +7 -0
- package/lib/types/client/settings-copy.d.ts.map +1 -0
- package/lib/types/client/settings-copy.js +53 -0
- package/lib/types/client/settings-copy.js.map +1 -0
- package/lib/types/client/settings-slots.d.ts +23 -0
- package/lib/types/client/settings-slots.d.ts.map +1 -0
- package/lib/types/client/settings-slots.js +2 -0
- package/lib/types/client/settings-slots.js.map +1 -0
- package/lib/types/client/settings.d.ts +4 -0
- package/lib/types/client/settings.d.ts.map +1 -0
- package/lib/types/client/settings.js +60 -0
- package/lib/types/client/settings.js.map +1 -0
- package/lib/types/client/skill-status.d.ts +25 -0
- package/lib/types/client/skill-status.d.ts.map +1 -0
- package/lib/types/client/skill-status.js +44 -0
- package/lib/types/client/skill-status.js.map +1 -0
- package/lib/types/client/store.d.ts +35 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/client/store.js +37 -0
- package/lib/types/client/store.js.map +1 -0
- package/lib/types/client/stream-buffer.d.ts +37 -0
- package/lib/types/client/stream-buffer.d.ts.map +1 -0
- package/lib/types/client/stream-buffer.js +150 -0
- package/lib/types/client/stream-buffer.js.map +1 -0
- package/lib/types/client/streaming.d.ts +18 -0
- package/lib/types/client/streaming.d.ts.map +1 -0
- package/lib/types/client/streaming.js +84 -0
- package/lib/types/client/streaming.js.map +1 -0
- package/lib/types/client/timeline.d.ts +26 -0
- package/lib/types/client/timeline.d.ts.map +1 -0
- package/lib/types/client/timeline.js +48 -0
- package/lib/types/client/timeline.js.map +1 -0
- package/lib/types/client/tool-activity.d.ts +98 -0
- package/lib/types/client/tool-activity.d.ts.map +1 -0
- package/lib/types/client/tool-activity.js +301 -0
- package/lib/types/client/tool-activity.js.map +1 -0
- package/lib/types/client/types.d.ts +91 -0
- package/lib/types/client/types.d.ts.map +1 -0
- package/lib/types/client/types.js +2 -0
- package/lib/types/client/types.js.map +1 -0
- package/lib/types/client/waiting-clock.d.ts +49 -0
- package/lib/types/client/waiting-clock.d.ts.map +1 -0
- package/lib/types/client/waiting-clock.js +80 -0
- package/lib/types/client/waiting-clock.js.map +1 -0
- package/lib/types/client/word-motion.d.ts +16 -0
- package/lib/types/client/word-motion.d.ts.map +1 -0
- package/lib/types/client/word-motion.js +90 -0
- package/lib/types/client/word-motion.js.map +1 -0
- package/lib/types/client/word-timeline.d.ts +32 -0
- package/lib/types/client/word-timeline.d.ts.map +1 -0
- package/lib/types/client/word-timeline.js +119 -0
- package/lib/types/client/word-timeline.js.map +1 -0
- package/lib/types/dsh-better-display.d.ts +17 -0
- package/lib/types/dsh-better-display.d.ts.map +1 -0
- package/lib/types/dsh-better-display.js +93 -0
- package/lib/types/dsh-better-display.js.map +1 -0
- package/lib/types/skill-roots.d.ts +22 -0
- package/lib/types/skill-roots.d.ts.map +1 -0
- package/lib/types/skill-roots.js +126 -0
- package/lib/types/skill-roots.js.map +1 -0
- package/lib/types/skill-status.d.ts +36 -0
- package/lib/types/skill-status.d.ts.map +1 -0
- package/lib/types/skill-status.js +51 -0
- package/lib/types/skill-status.js.map +1 -0
- package/package.json +133 -0
- package/scripts/build-motion-fixture.mjs +26 -0
- package/scripts/check-harness-compat.mjs +71 -0
- package/scripts/link-harness-dependencies.mjs +54 -0
- package/scripts/verify-auto-fold.mjs +114 -0
- package/scripts/verify-official-bridge.mjs +121 -0
- package/scripts/verify-tool-shimmer.mjs +62 -0
- package/skills/generative-mcpapps/SKILL.md +47 -0
- package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
- package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
- package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
- package/src/client/Blocks.tsx +213 -0
- package/src/client/ChoreographedFlow.tsx +232 -0
- package/src/client/ClosedProcessSummary.tsx +23 -0
- package/src/client/DiffPanel.tsx +124 -0
- package/src/client/LiveFold.tsx +43 -0
- package/src/client/McpAppFrame.module.css +210 -0
- package/src/client/McpAppFrame.tsx +317 -0
- package/src/client/OfficialContent.tsx +69 -0
- package/src/client/Reader.module.css +692 -0
- package/src/client/Reader.tsx +878 -0
- package/src/client/ReasoningCard.tsx +306 -0
- package/src/client/SettingsSection.module.css +184 -0
- package/src/client/SettingsSection.tsx +194 -0
- package/src/client/StickyLane.tsx +34 -0
- package/src/client/TimelineRail.module.css +170 -0
- package/src/client/TimelineRail.tsx +262 -0
- package/src/client/ToolActivity.tsx +274 -0
- package/src/client/TurnMetrics.module.css +139 -0
- package/src/client/TurnMetrics.tsx +160 -0
- package/src/client/WaitClock.tsx +33 -0
- package/src/client/WaitingStatus.tsx +13 -0
- package/src/client/conversation-scroll.ts +28 -0
- package/src/client/deliverables.ts +122 -0
- package/src/client/entry-policy.ts +20 -0
- package/src/client/entry.tsx +40 -0
- package/src/client/fold-choreography.ts +55 -0
- package/src/client/fold-intensity.ts +36 -0
- package/src/client/index.tsx +222 -0
- package/src/client/live-turn.ts +198 -0
- package/src/client/markdown/MarkdownText.module.css +304 -0
- package/src/client/markdown/MarkdownText.tsx +195 -0
- package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
- package/src/client/markdown/incremental.ts +131 -0
- package/src/client/markdown/katex.tsx +91 -0
- package/src/client/markdown/mathCompatibility.ts +350 -0
- package/src/client/markdown/parse.ts +45 -0
- package/src/client/markdown/render.tsx +647 -0
- package/src/client/markdown/upstream.json +14 -0
- package/src/client/mcp-app.ts +323 -0
- package/src/client/message-chrome.ts +47 -0
- package/src/client/motion.tsx +410 -0
- package/src/client/native/ContextBody.module.css +161 -0
- package/src/client/native/ContextBody.tsx +591 -0
- package/src/client/native/ContextInjectionRow.module.css +66 -0
- package/src/client/native/ContextInjectionRow.tsx +106 -0
- package/src/client/native/ReferenceIcon.tsx +35 -0
- package/src/client/native/tool-call-model.ts +248 -0
- package/src/client/native/tool-update.ts +32 -0
- package/src/client/native/upstream.json +41 -0
- package/src/client/official-slots.tsx +211 -0
- package/src/client/open-file.ts +94 -0
- package/src/client/pending-submission.ts +49 -0
- package/src/client/platform-media.ts +17 -0
- package/src/client/primitive-labels.ts +54 -0
- package/src/client/projection.ts +118 -0
- package/src/client/reasoning-follow.ts +28 -0
- package/src/client/settings-copy.ts +80 -0
- package/src/client/settings-slots.ts +20 -0
- package/src/client/settings.tsx +73 -0
- package/src/client/skill-status.ts +78 -0
- package/src/client/store.ts +75 -0
- package/src/client/stream-buffer.ts +136 -0
- package/src/client/streaming.tsx +70 -0
- package/src/client/style.d.ts +4 -0
- package/src/client/timeline.ts +77 -0
- package/src/client/tool-activity.ts +298 -0
- package/src/client/types.ts +89 -0
- package/src/client/waiting-clock.ts +83 -0
- package/src/client/word-motion.tsx +92 -0
- package/src/client/word-timeline.ts +113 -0
- package/src/dsh-better-display.ts +108 -0
- package/src/skill-roots.ts +138 -0
- package/src/skill-status.ts +81 -0
- package/tests/browser-auto-fold.tsx +64 -0
- package/tests/browser-closed-gap.tsx +20 -0
- package/tests/browser-motion.tsx +65 -0
- package/tests/browser-official-bridge.tsx +152 -0
- package/tests/browser-sticky-lanes.tsx +22 -0
- package/tests/browser-waiting-clock.tsx +14 -0
- package/tests/chat-flow-hook.test.ts +66 -0
- package/tests/cloud-gate.test.ts +34 -0
- package/tests/context-tool-update.test.ts +33 -0
- package/tests/conversation-scroll.test.ts +47 -0
- package/tests/deliverables.test.ts +128 -0
- package/tests/entry.test.ts +42 -0
- package/tests/fixtures/maid-composer-hide.html +31 -0
- package/tests/fold-choreography.test.ts +54 -0
- package/tests/fold-intensity.test.ts +94 -0
- package/tests/footer-precedence.test.ts +129 -0
- package/tests/history-word-regression.test.ts +16 -0
- package/tests/live-turn.test.ts +218 -0
- package/tests/mcp-apps.test.ts +157 -0
- package/tests/message-chrome.test.ts +29 -0
- package/tests/official-slots.test.ts +112 -0
- package/tests/open-file.test.ts +114 -0
- package/tests/pending-submission.test.ts +45 -0
- package/tests/platform-media.test.ts +23 -0
- package/tests/projection.test.ts +184 -0
- package/tests/reasoning-follow.test.ts +34 -0
- package/tests/settings-copy.test.ts +63 -0
- package/tests/skill-status.test.ts +137 -0
- package/tests/stock-install.test.ts +58 -0
- package/tests/stream-buffer.test.ts +102 -0
- package/tests/timeline.test.ts +59 -0
- package/tests/tool-diff.test.ts +57 -0
- package/tests/waiting-clock.test.ts +83 -0
- package/tests/word-timeline.test.ts +146 -0
- package/tsconfig.json +11 -0
- package/tsdown.config.ts +51 -0
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
const animation = element.animate(
|
|
130
|
+
from > target
|
|
131
|
+
? [
|
|
132
|
+
{ offset: 0, height: `${from}px`, opacity: 1, transform: 'scaleY(1) translateY(0)' },
|
|
133
|
+
{ offset: 0.32, height: `${Math.round(from * 0.92)}px`, opacity: 0.35, transform: 'scaleY(0.96) translateY(-2px)' },
|
|
134
|
+
{ offset: 1, height: `${target}px`, opacity: 0, transform: 'scaleY(0.68) translateY(-8px)' },
|
|
135
|
+
]
|
|
136
|
+
: [
|
|
137
|
+
{ offset: 0, height: `${from}px`, opacity: 0, transform: 'scaleY(0.8) translateY(6px)' },
|
|
138
|
+
{ offset: 0.3, height: `${Math.round(target * 0.4)}px`, opacity: 0.4, transform: 'scaleY(0.92) translateY(3px)' },
|
|
139
|
+
{ offset: 1, height: `${target}px`, opacity: 1, transform: 'scaleY(1) translateY(0)' },
|
|
140
|
+
],
|
|
141
|
+
{ duration: 380, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'both' },
|
|
142
|
+
);
|
|
143
|
+
running.current = animation;
|
|
144
|
+
let settled = false;
|
|
145
|
+
const settle = () => {
|
|
146
|
+
if (settled) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
if (running.current === animation) running.current = null;
|
|
149
|
+
animation.cancel();
|
|
150
|
+
setPresent(open);
|
|
151
|
+
};
|
|
152
|
+
animation.onfinish = settle;
|
|
153
|
+
// `fill: 'both'\` pins the opening keyframe (height 0, opacity 0), so while this
|
|
154
|
+
// animation runs the body is *invisible* no matter what the element's style says.
|
|
155
|
+
// An animation that never finishes — cancelled by a re-run, an unmount, or a
|
|
156
|
+
// compositor that drops it — would then leave the row looking like it refused to
|
|
157
|
+
// open, which is exactly the reported symptom. Cancel is only safe once the
|
|
158
|
+
// state change has been committed, so the deadline does both.
|
|
159
|
+
const deadline = window.setTimeout(settle, 380 + 240);
|
|
160
|
+
return () => {
|
|
161
|
+
window.clearTimeout(deadline);
|
|
162
|
+
running.current?.cancel();
|
|
163
|
+
};
|
|
164
|
+
}, [open, motion, returnFocusTo]);
|
|
165
|
+
if (!open && !present) return null;
|
|
166
|
+
return <div ref={body} className={css.disclosureBody} data-reader-process data-reader-process-key={nodeKey} data-ud-motion="reader-process-size"
|
|
167
|
+
aria-hidden={!open} onPointerDown={() => { if (open) onRead(); }} onFocusCapture={() => { if (open) onRead(); }} {...(!open ? { inert: '' } : {})}>
|
|
168
|
+
<div className={framed ? css.processFrame : css.processContents}>{children}</div>
|
|
169
|
+
</div>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Retire only narration that was actually visible; historical rows stay folded. */
|
|
173
|
+
export function RetiringContent({ visible, children }: { visible: boolean; children: ReactNode }) {
|
|
174
|
+
const { enabled } = useContext(StreamMotionContext);
|
|
175
|
+
const root = useRef<HTMLDivElement>(null);
|
|
176
|
+
const animation = useRef<Animation | null>(null);
|
|
177
|
+
const [present, setPresent] = useState(visible);
|
|
178
|
+
const [focusHeld, setFocusHeld] = useState(false);
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
const element = root.current;
|
|
181
|
+
if (visible) {
|
|
182
|
+
animation.current?.cancel(); animation.current = null;
|
|
183
|
+
setPresent(true);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!element) return;
|
|
187
|
+
if (element.contains(document.activeElement)) { setFocusHeld(true); return; }
|
|
188
|
+
if (focusHeld) return;
|
|
189
|
+
const from = element.getBoundingClientRect().height;
|
|
190
|
+
animation.current?.cancel(); animation.current = null;
|
|
191
|
+
if (!enabled || from < 1) { setPresent(false); return; }
|
|
192
|
+
const next = element.animate([{ height: `${from}px`, opacity: 1 }, { height: '0px', opacity: 0 }], { duration: 220, easing: EASING, fill: 'both' });
|
|
193
|
+
animation.current = next;
|
|
194
|
+
next.onfinish = () => { if (animation.current === next) { animation.current = null; next.cancel(); setPresent(false); } };
|
|
195
|
+
}, [visible, enabled, focusHeld]);
|
|
196
|
+
useEffect(() => () => animation.current?.cancel(), []);
|
|
197
|
+
if (!visible && !present) return null;
|
|
198
|
+
return <div ref={root} className={css.retiringContent} data-reader-retiring={visible ? 'visible' : 'retiring'} data-ud-motion="reader-progress-retire"
|
|
199
|
+
onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) setFocusHeld(false); }}>
|
|
200
|
+
{children}
|
|
201
|
+
</div>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// DOM-only behavior: the native Session remains the sole source of business data.
|
|
205
|
+
export function useReadingScroll(root: RefObject<HTMLElement>, motion: boolean): {
|
|
206
|
+
detached: boolean;
|
|
207
|
+
jump: () => void;
|
|
208
|
+
/** Stop tail-follow so a rail landing is not pulled back to the live bottom. */
|
|
209
|
+
release: () => void;
|
|
210
|
+
} {
|
|
211
|
+
const port = useRef<HTMLElement | null>(null);
|
|
212
|
+
const following = useRef(true);
|
|
213
|
+
// Tracked separately from `following`: only an explicit reader action clears
|
|
214
|
+
// this, so a fold-induced scroll clamp cannot permanently drop the tail.
|
|
215
|
+
const pinned = useRef(true);
|
|
216
|
+
const anchor = useRef<{ element: HTMLElement; docTop: number } | null>(null);
|
|
217
|
+
const cancelFollow = useRef<() => void>(() => {});
|
|
218
|
+
const [detached, setDetached] = useState(false);
|
|
219
|
+
useLayoutEffect(() => {
|
|
220
|
+
const content = root.current;
|
|
221
|
+
if (!content) return;
|
|
222
|
+
const scroll = content.closest<HTMLElement>('[data-conversation-scroll]') ?? content;
|
|
223
|
+
port.current = scroll;
|
|
224
|
+
// Browser scroll anchoring moves the viewport on its own as the transcript
|
|
225
|
+
// grows, which reads as the page freezing mid-follow.
|
|
226
|
+
const previousOverflowAnchor = scroll.style.overflowAnchor;
|
|
227
|
+
scroll.style.overflowAnchor = 'none';
|
|
228
|
+
let followFrame = 0;
|
|
229
|
+
let lastFrameAt = 0;
|
|
230
|
+
let layoutDepth = 0;
|
|
231
|
+
let lastWrittenTop: number | null = null;
|
|
232
|
+
const layoutStart = () => {
|
|
233
|
+
layoutDepth++;
|
|
234
|
+
cancelAnimationFrame(followFrame); followFrame = 0;
|
|
235
|
+
capture();
|
|
236
|
+
};
|
|
237
|
+
const layoutEnd = () => {
|
|
238
|
+
layoutDepth = Math.max(0, layoutDepth - 1);
|
|
239
|
+
if (!layoutDepth && following.current && !followFrame) {
|
|
240
|
+
lastFrameAt = performance.now(); followFrame = requestAnimationFrame(follow);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
cancelFollow.current = () => {
|
|
244
|
+
cancelAnimationFrame(followFrame);
|
|
245
|
+
followFrame = 0;
|
|
246
|
+
lastWrittenTop = null;
|
|
247
|
+
};
|
|
248
|
+
const selected = () => {
|
|
249
|
+
const selection = document.getSelection();
|
|
250
|
+
return selection && !selection.isCollapsed && selection.anchorNode && content.contains(selection.anchorNode);
|
|
251
|
+
};
|
|
252
|
+
// Only a focused text field suspends tail-follow. Clicking a fold control
|
|
253
|
+
// inside the reader also moves document.activeElement into `content`, and
|
|
254
|
+
// treating that as "user is busy" froze following for the rest of the turn.
|
|
255
|
+
const editingText = () => {
|
|
256
|
+
const active = document.activeElement;
|
|
257
|
+
if (!(active instanceof HTMLElement)) return false;
|
|
258
|
+
// The composer lives outside this subtree: focusing it must never stop
|
|
259
|
+
// the transcript from following.
|
|
260
|
+
if (!content.contains(active)) return false;
|
|
261
|
+
return active.isContentEditable || active.tagName === 'TEXTAREA' || active.tagName === 'INPUT';
|
|
262
|
+
};
|
|
263
|
+
const capture = () => {
|
|
264
|
+
const top = scroll.getBoundingClientRect().top;
|
|
265
|
+
const candidates = content.querySelectorAll<HTMLElement>('[data-reader-anchor]');
|
|
266
|
+
let candidate: HTMLElement | null = null;
|
|
267
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
268
|
+
const element = candidates[index];
|
|
269
|
+
if (element.getBoundingClientRect().bottom > top + 8) { candidate = element; break; }
|
|
270
|
+
}
|
|
271
|
+
// Record the anchor in DOCUMENT space, never viewport space. The viewport
|
|
272
|
+
// offset also changes whenever the reader scrolls, so comparing it back
|
|
273
|
+
// turned a plain scroll into an "anchor correction" that yanked the
|
|
274
|
+
// viewport back to wherever the anchor had last been captured.
|
|
275
|
+
anchor.current = candidate
|
|
276
|
+
? { element: candidate, docTop: candidate.getBoundingClientRect().top + scroll.scrollTop }
|
|
277
|
+
: null;
|
|
278
|
+
};
|
|
279
|
+
// Anchor capture is only consumed while detached; coalesce DOM scans to one per frame.
|
|
280
|
+
let captureFrame = 0;
|
|
281
|
+
const scheduleCapture = () => {
|
|
282
|
+
if (captureFrame) return;
|
|
283
|
+
captureFrame = requestAnimationFrame(() => { captureFrame = 0; capture(); });
|
|
284
|
+
};
|
|
285
|
+
const atBottom = () => scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 25;
|
|
286
|
+
// Intent to stay pinned to the bottom, tracked independently of the scroll
|
|
287
|
+
// position. A fold collapse shrinks the transcript and the browser clamps
|
|
288
|
+
// scrollTop, producing a scroll event indistinguishable from the reader
|
|
289
|
+
// scrolling up; without this the reader silently detached exactly when the
|
|
290
|
+
// turn's last content arrived, and the page stopped following.
|
|
291
|
+
pinned.current = true;
|
|
292
|
+
const onScroll = () => {
|
|
293
|
+
// While a follow animation is actively driving scroll, do not cancel following midway.
|
|
294
|
+
if (followFrame !== 0 || layoutDepth > 0) return;
|
|
295
|
+
// Our easing frames must not be mistaken for a user leaving the bottom.
|
|
296
|
+
if (lastWrittenTop !== null && Math.abs(scroll.scrollTop - lastWrittenTop) < 1) return;
|
|
297
|
+
// Bind the follow intent to the position observed HERE. Every scroll that
|
|
298
|
+
// reaches this point is the viewport moving under someone else's hand: a
|
|
299
|
+
// wheel, a drag, a key, or the browser clamping the offset after a shrink.
|
|
300
|
+
//
|
|
301
|
+
// The previous form inferred detaching from a strictly upward delta against
|
|
302
|
+
// lastWrittenTop, which left two holes. That baseline is nulled by
|
|
303
|
+
// cancelFollow/onWheel/onTouch/onKey, and the guard above already returned
|
|
304
|
+
// for any scroll whose delta is under 1px — so a reader parked mid-transcript
|
|
305
|
+
// by a scrollbar drag stayed following === true AND pinned === true. The next
|
|
306
|
+
// content growth then took the re-attach branch and yanked the viewport to
|
|
307
|
+
// the bottom, discarding what they were reading. Reading the position
|
|
308
|
+
// directly needs no baseline: a fold's clamp lands at the very bottom and
|
|
309
|
+
// still keeps following, while any other offset detaches.
|
|
310
|
+
const bottom = atBottom();
|
|
311
|
+
pinned.current = bottom;
|
|
312
|
+
if (!bottom) { cancelAnimationFrame(followFrame); followFrame = 0; scheduleCapture(); }
|
|
313
|
+
if (!following.current) {
|
|
314
|
+
// Only an explicit return to the bottom resumes following.
|
|
315
|
+
if (bottom) { following.current = true; setDetached(false); }
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!bottom) { following.current = false; setDetached(true); }
|
|
319
|
+
};
|
|
320
|
+
const onWheel = (event: WheelEvent) => {
|
|
321
|
+
cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
|
|
322
|
+
if (event.deltaY < 0) { following.current = false; setDetached(true); pinned.current = false; scheduleCapture(); }
|
|
323
|
+
};
|
|
324
|
+
const onTouch = () => {
|
|
325
|
+
cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
|
|
326
|
+
};
|
|
327
|
+
const onKey = (event: KeyboardEvent) => {
|
|
328
|
+
if (event.target instanceof HTMLElement && event.target.closest('textarea,input,[contenteditable=true]')) return;
|
|
329
|
+
if (['PageUp', 'Home', 'ArrowUp'].includes(event.key)) {
|
|
330
|
+
cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
|
|
331
|
+
following.current = false; setDetached(true); pinned.current = false; scheduleCapture();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const writeTop = (top: number) => { scroll.scrollTop = top; lastWrittenTop = scroll.scrollTop; };
|
|
335
|
+
const follow = (now: number) => {
|
|
336
|
+
followFrame = 0;
|
|
337
|
+
if (!following.current || selected() || editingText()) return;
|
|
338
|
+
if (layoutDepth > 0) return;
|
|
339
|
+
const gap = scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop;
|
|
340
|
+
const delta = Math.min(48, Math.max(1, now - lastFrameAt));
|
|
341
|
+
lastFrameAt = now;
|
|
342
|
+
if (!motion || Math.abs(gap) < 1.5) { writeTop(scroll.scrollHeight); capture(); return; }
|
|
343
|
+
writeTop(scroll.scrollTop + gap * (1 - Math.exp(-delta / 52)));
|
|
344
|
+
followFrame = requestAnimationFrame(follow);
|
|
345
|
+
};
|
|
346
|
+
const firstFrame = requestAnimationFrame(() => {
|
|
347
|
+
if (following.current && !selected()) writeTop(scroll.scrollHeight);
|
|
348
|
+
capture();
|
|
349
|
+
});
|
|
350
|
+
let lastHeight = content.scrollHeight;
|
|
351
|
+
const observer = new ResizeObserver(() => {
|
|
352
|
+
if (selected()) return;
|
|
353
|
+
const height = scroll.scrollHeight;
|
|
354
|
+
const grew = height > lastHeight + 1;
|
|
355
|
+
lastHeight = height;
|
|
356
|
+
// Growth while the reader intends to be at the bottom re-attaches the
|
|
357
|
+
// tail. This is the self-healing path for the clamp case above and is
|
|
358
|
+
// also what keeps the viewport moving when a turn ends.
|
|
359
|
+
if (grew && pinned.current && !editingText()) { following.current = true; setDetached(false); }
|
|
360
|
+
if (following.current && !editingText()) {
|
|
361
|
+
// Height is already animated: no second, lagging scroll easing.
|
|
362
|
+
if (!motion || layoutDepth > 0) writeTop(scroll.scrollHeight);
|
|
363
|
+
else if (!followFrame) { lastFrameAt = performance.now(); followFrame = requestAnimationFrame(follow); }
|
|
364
|
+
} else if (!following.current && anchor.current?.element.isConnected) {
|
|
365
|
+
// Only a real layout change above the anchor needs compensation. A reader
|
|
366
|
+
// scroll moves the viewport offset but not the document offset, so this
|
|
367
|
+
// stays silent while the reader is scrolling and cannot snap back.
|
|
368
|
+
const delta = anchor.current.element.getBoundingClientRect().top + scroll.scrollTop - anchor.current.docTop;
|
|
369
|
+
if (Math.abs(delta) > .5) writeTop(scroll.scrollTop + delta);
|
|
370
|
+
}
|
|
371
|
+
capture();
|
|
372
|
+
});
|
|
373
|
+
observer.observe(content);
|
|
374
|
+
if (scroll !== content) observer.observe(scroll);
|
|
375
|
+
content.addEventListener('reader-layout-start', layoutStart);
|
|
376
|
+
content.addEventListener('reader-layout-end', layoutEnd);
|
|
377
|
+
scroll.addEventListener('scroll', onScroll, { passive: true });
|
|
378
|
+
scroll.addEventListener('wheel', onWheel, { passive: true });
|
|
379
|
+
scroll.addEventListener('touchstart', onTouch, { passive: true });
|
|
380
|
+
scroll.addEventListener('touchmove', onTouch, { passive: true });
|
|
381
|
+
scroll.addEventListener('keydown', onKey);
|
|
382
|
+
return () => {
|
|
383
|
+
cancelAnimationFrame(firstFrame); cancelAnimationFrame(followFrame); cancelAnimationFrame(captureFrame); observer.disconnect();
|
|
384
|
+
content.removeEventListener('reader-layout-start', layoutStart);
|
|
385
|
+
content.removeEventListener('reader-layout-end', layoutEnd);
|
|
386
|
+
scroll.style.overflowAnchor = previousOverflowAnchor;
|
|
387
|
+
scroll.removeEventListener('scroll', onScroll); scroll.removeEventListener('wheel', onWheel);
|
|
388
|
+
scroll.removeEventListener('touchstart', onTouch); scroll.removeEventListener('touchmove', onTouch);
|
|
389
|
+
scroll.removeEventListener('keydown', onKey);
|
|
390
|
+
};
|
|
391
|
+
}, [root, motion]);
|
|
392
|
+
const jump = useCallback(() => {
|
|
393
|
+
cancelFollow.current();
|
|
394
|
+
anchor.current = null;
|
|
395
|
+
following.current = true;
|
|
396
|
+
pinned.current = true;
|
|
397
|
+
setDetached(false);
|
|
398
|
+
if (port.current) {
|
|
399
|
+
port.current.scrollTop = port.current.scrollHeight;
|
|
400
|
+
}
|
|
401
|
+
}, []);
|
|
402
|
+
const release = useCallback(() => {
|
|
403
|
+
cancelFollow.current();
|
|
404
|
+
following.current = false;
|
|
405
|
+
pinned.current = false;
|
|
406
|
+
anchor.current = null;
|
|
407
|
+
setDetached(true);
|
|
408
|
+
}, []);
|
|
409
|
+
return { detached, jump, release };
|
|
410
|
+
}
|
|
@@ -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
|
+
}
|