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.
- package/CHANGELOG.md +51 -0
- package/DESIGN.md +72 -0
- package/LICENSE +24 -0
- package/README.en.md +41 -0
- package/README.md +41 -0
- package/THIRD_PARTY_NOTICES.md +65 -0
- package/cordis.patch.yml +5 -0
- package/cordis.yml +4 -0
- package/dshx.yml +5 -0
- package/lib/client.js +49923 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-better-display.js +256 -0
- package/lib/types/client/Blocks.d.ts +45 -0
- package/lib/types/client/Blocks.d.ts.map +1 -0
- package/lib/types/client/Blocks.js +136 -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 +170 -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 +15 -0
- package/lib/types/client/ClosedProcessSummary.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/Reader.d.ts +3 -0
- package/lib/types/client/Reader.d.ts.map +1 -0
- package/lib/types/client/Reader.js +537 -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 +301 -0
- package/lib/types/client/ReasoningCard.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 +28 -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 +192 -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/WaitingStatus.d.ts +6 -0
- package/lib/types/client/WaitingStatus.d.ts.map +1 -0
- package/lib/types/client/WaitingStatus.js +19 -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 +28 -0
- package/lib/types/client/fold-choreography.d.ts.map +1 -0
- package/lib/types/client/fold-choreography.js +31 -0
- package/lib/types/client/fold-choreography.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 +153 -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 +144 -0
- package/lib/types/client/live-turn.js.map +1 -0
- package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
- package/lib/types/client/markdown/MarkdownText.js +171 -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 +119 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -0
- package/lib/types/client/markdown/render.js +435 -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 +367 -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 +26 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.js +27 -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/open-file.d.ts +24 -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 +37 -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 +6 -0
- package/lib/types/client/reasoning-follow.d.ts.map +1 -0
- package/lib/types/client/reasoning-follow.js +12 -0
- package/lib/types/client/reasoning-follow.js.map +1 -0
- package/lib/types/client/settings-copy.d.ts +6 -0
- package/lib/types/client/settings-slots.d.ts +16 -0
- package/lib/types/client/store.d.ts +17 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/client/store.js +13 -0
- package/lib/types/client/store.js.map +1 -0
- package/lib/types/client/stream-buffer.d.ts +29 -0
- package/lib/types/client/stream-buffer.d.ts.map +1 -0
- package/lib/types/client/stream-buffer.js +113 -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 +77 -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 +52 -0
- package/lib/types/client/tool-activity.d.ts.map +1 -0
- package/lib/types/client/tool-activity.js +178 -0
- package/lib/types/client/tool-activity.js.map +1 -0
- package/lib/types/client/types.d.ts +66 -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 +17 -0
- package/lib/types/client/waiting-clock.d.ts.map +1 -0
- package/lib/types/client/waiting-clock.js +24 -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 +89 -0
- package/lib/types/client/word-motion.js.map +1 -0
- package/lib/types/client/word-timeline.d.ts +28 -0
- package/lib/types/client/word-timeline.d.ts.map +1 -0
- package/lib/types/client/word-timeline.js +100 -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 +53 -0
- package/lib/types/dsh-better-display.js.map +1 -0
- package/package.json +121 -0
- package/scripts/build-motion-fixture.mjs +26 -0
- package/scripts/link-harness-dependencies.mjs +51 -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 +205 -0
- package/src/client/ChoreographedFlow.tsx +182 -0
- package/src/client/ClosedProcessSummary.tsx +19 -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/Reader.module.css +329 -0
- package/src/client/Reader.tsx +814 -0
- package/src/client/ReasoningCard.tsx +285 -0
- package/src/client/SettingsSection.module.css +147 -0
- package/src/client/SettingsSection.tsx +117 -0
- package/src/client/StickyLane.tsx +24 -0
- package/src/client/TimelineRail.module.css +170 -0
- package/src/client/TimelineRail.tsx +262 -0
- package/src/client/ToolActivity.tsx +214 -0
- package/src/client/TurnMetrics.module.css +139 -0
- package/src/client/TurnMetrics.tsx +160 -0
- package/src/client/WaitingStatus.tsx +22 -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 +38 -0
- package/src/client/index.tsx +198 -0
- package/src/client/live-turn.ts +155 -0
- package/src/client/markdown/MarkdownText.module.css +304 -0
- package/src/client/markdown/MarkdownText.tsx +187 -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 +630 -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 +338 -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 +62 -0
- package/src/client/native/ContextInjectionRow.tsx +72 -0
- package/src/client/native/ReferenceIcon.tsx +35 -0
- package/src/client/native/tool-call-model.ts +248 -0
- package/src/client/native/upstream.json +41 -0
- package/src/client/open-file.ts +94 -0
- package/src/client/pending-submission.ts +49 -0
- package/src/client/primitive-labels.ts +53 -0
- package/src/client/projection.ts +118 -0
- package/src/client/reasoning-follow.ts +12 -0
- package/src/client/settings-copy.ts +53 -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 +30 -0
- package/src/client/stream-buffer.ts +100 -0
- package/src/client/streaming.tsx +62 -0
- package/src/client/style.d.ts +4 -0
- package/src/client/timeline.ts +77 -0
- package/src/client/tool-activity.ts +152 -0
- package/src/client/types.ts +66 -0
- package/src/client/waiting-clock.ts +25 -0
- package/src/client/word-motion.tsx +91 -0
- package/src/client/word-timeline.ts +96 -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-closed-gap.tsx +20 -0
- package/tests/browser-motion.tsx +65 -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/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 +38 -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/open-file.test.ts +103 -0
- package/tests/pending-submission.test.ts +45 -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 +50 -0
- package/tests/stream-buffer.test.ts +97 -0
- package/tests/timeline.test.ts +59 -0
- package/tests/waiting-clock.test.ts +24 -0
- package/tests/word-timeline.test.ts +136 -0
- package/tsconfig.json +11 -0
- 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
|
+
}
|