papilot 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -2
- package/README.zh-CN.md +127 -0
- package/bin/papilot.mjs +113 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Inconsolatazi4-Regular-Dbtq7t_r.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Italic-CSuYIstn.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/dist/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
- package/dist/assets/index-BhQFCMTx.js +131 -0
- package/dist/assets/index-CAV3-87U.css +1 -0
- package/dist/assets/pdf-view-DJo0X38w.js +598 -0
- package/dist/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +4 -0
- package/examples/papilot-paper/.latex-review.json +4 -0
- package/examples/papilot-paper/appendix/proofs.tex +60 -0
- package/examples/papilot-paper/appendix/reference.tex +54 -0
- package/examples/papilot-paper/main.tex +57 -0
- package/examples/papilot-paper/preamble.tex +53 -0
- package/examples/papilot-paper/references.bib +158 -0
- package/examples/papilot-paper/sections/01-introduction.tex +66 -0
- package/examples/papilot-paper/sections/02-using.tex +134 -0
- package/examples/papilot-paper/sections/03-background.tex +97 -0
- package/examples/papilot-paper/sections/04-model.tex +185 -0
- package/examples/papilot-paper/sections/05-conflicts.tex +99 -0
- package/examples/papilot-paper/sections/06-building.tex +70 -0
- package/examples/papilot-paper/sections/07-geometry.tex +193 -0
- package/examples/papilot-paper/sections/08-notes.tex +104 -0
- package/examples/papilot-paper/sections/09-agents.tex +106 -0
- package/examples/papilot-paper/sections/10-usage.tex +56 -0
- package/examples/papilot-paper/sections/11-git.tex +62 -0
- package/examples/papilot-paper/sections/12-security.tex +41 -0
- package/examples/papilot-paper/sections/13-evaluation.tex +106 -0
- package/examples/papilot-paper/sections/14-related.tex +38 -0
- package/examples/papilot-paper/sections/15-conclusion.tex +27 -0
- package/package.json +85 -4
- package/scripts/demo.ts +21 -0
- package/scripts/service.ts +35 -0
- package/src/notes.ts +40 -0
- package/src/server/agents.ts +343 -0
- package/src/server/codex.ts +63 -0
- package/src/server/frontend.ts +15 -0
- package/src/server/main.ts +202 -0
- package/src/server/manuscript.ts +337 -0
- package/src/server/paths.ts +10 -0
- package/src/server/pdf.ts +27 -0
- package/src/server/review.ts +53 -0
- package/src/server/security.ts +15 -0
- package/src/server/store.ts +60 -0
- package/src/server/workspace.ts +529 -0
- package/src/shared.ts +28 -0
- package/vendor/latex-review/LICENSE +21 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +19 -0
- package/vendor/latex-review/dist/web/index.html +18 -0
- package/vendor/latex-review/package-lock.json +3114 -0
- package/vendor/latex-review/package.json +49 -0
- package/vendor/latex-review/src/cli.ts +494 -0
- package/vendor/latex-review/src/client.ts +198 -0
- package/vendor/latex-review/src/core/build.ts +296 -0
- package/vendor/latex-review/src/core/changes.ts +126 -0
- package/vendor/latex-review/src/core/config.ts +270 -0
- package/vendor/latex-review/src/core/diff.ts +54 -0
- package/vendor/latex-review/src/core/files.ts +96 -0
- package/vendor/latex-review/src/core/git.ts +317 -0
- package/vendor/latex-review/src/core/outline.ts +314 -0
- package/vendor/latex-review/src/core/process.ts +111 -0
- package/vendor/latex-review/src/core/publish.ts +339 -0
- package/vendor/latex-review/src/core/review.ts +734 -0
- package/vendor/latex-review/src/core/store.ts +66 -0
- package/vendor/latex-review/src/core/tasks.ts +237 -0
- package/vendor/latex-review/src/render/inline.ts +805 -0
- package/vendor/latex-review/src/render/locate.ts +481 -0
- package/vendor/latex-review/src/render/pdf.ts +281 -0
- package/vendor/latex-review/src/render/pdfdiff.ts +272 -0
- package/vendor/latex-review/src/render/round.ts +236 -0
- package/vendor/latex-review/src/render/worker.ts +10 -0
- package/vendor/latex-review/src/runtime.ts +23 -0
- package/vendor/latex-review/src/server.ts +401 -0
- package/vendor/latex-review/src/types.ts +283 -0
- package/vendor/latex-review/src/web/ChangeMap.tsx +212 -0
- package/vendor/latex-review/src/web/PdfStrip.tsx +77 -0
- package/vendor/latex-review/src/web/fonts/GUST-FONT-LICENSE.txt +29 -0
- package/vendor/latex-review/src/web/fonts/LPPL.txt +415 -0
- package/vendor/latex-review/src/web/fonts/MANIFEST-newpx.txt +20 -0
- package/vendor/latex-review/src/web/fonts/README-newpx.txt +473 -0
- package/vendor/latex-review/src/web/fonts/README.md +12 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
- package/vendor/latex-review/src/web/index.html +17 -0
- package/vendor/latex-review/src/web/main.tsx +10 -0
- package/vendor/latex-review/src/web/model.ts +153 -0
- package/vendor/latex-review/src/web/overview.tsx +146 -0
- package/vendor/latex-review/src/web/review.tsx +991 -0
- package/vendor/latex-review/src/web/style.css +1249 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
export type Span = [number, number];
|
|
2
|
+
export type Rect = [number, number, number, number];
|
|
3
|
+
export type SourceBox = [
|
|
4
|
+
page: number,
|
|
5
|
+
top: number,
|
|
6
|
+
bottom: number,
|
|
7
|
+
left?: number,
|
|
8
|
+
right?: number,
|
|
9
|
+
];
|
|
10
|
+
export type Ranges = Record<string, Span[]>;
|
|
11
|
+
export type Op = [
|
|
12
|
+
"equal" | "replace" | "insert" | "delete",
|
|
13
|
+
number,
|
|
14
|
+
number,
|
|
15
|
+
number,
|
|
16
|
+
number,
|
|
17
|
+
];
|
|
18
|
+
export interface Heading {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
number: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
level: number;
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
parent: string | null;
|
|
27
|
+
labels: string[];
|
|
28
|
+
ranges: Ranges;
|
|
29
|
+
files: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface SectionRef {
|
|
32
|
+
id: string;
|
|
33
|
+
number: string;
|
|
34
|
+
title: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Scope {
|
|
37
|
+
paths: string[];
|
|
38
|
+
ranges: Ranges | null;
|
|
39
|
+
sections: SectionRef[];
|
|
40
|
+
}
|
|
41
|
+
export interface Task {
|
|
42
|
+
name: string;
|
|
43
|
+
title: string;
|
|
44
|
+
worktree: string;
|
|
45
|
+
base: string;
|
|
46
|
+
accepted: string;
|
|
47
|
+
scope: Scope;
|
|
48
|
+
created: number;
|
|
49
|
+
closed: number | null;
|
|
50
|
+
}
|
|
51
|
+
export interface Unit {
|
|
52
|
+
id: string;
|
|
53
|
+
file: string;
|
|
54
|
+
status: "added" | "deleted" | "modified";
|
|
55
|
+
kind: "text" | "figure" | "binary";
|
|
56
|
+
whole_file: boolean;
|
|
57
|
+
ops: Op[];
|
|
58
|
+
old_start: number;
|
|
59
|
+
old_end: number;
|
|
60
|
+
new_start: number;
|
|
61
|
+
new_end: number;
|
|
62
|
+
old_text: string;
|
|
63
|
+
new_text: string;
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface Round {
|
|
67
|
+
task: string;
|
|
68
|
+
number: number;
|
|
69
|
+
before: string;
|
|
70
|
+
after: string;
|
|
71
|
+
units: Unit[];
|
|
72
|
+
summary: { warnings: string[]; targets: string[]; builds: string[] };
|
|
73
|
+
created: number;
|
|
74
|
+
}
|
|
75
|
+
export type ActionKind = "accept" | "reject" | "comment";
|
|
76
|
+
export type Decision = "accepted" | "rejected" | "commented" | "pending";
|
|
77
|
+
export interface ActionDetail {
|
|
78
|
+
accepted_before?: string;
|
|
79
|
+
accepted_after?: string;
|
|
80
|
+
not_reverted?: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface Action {
|
|
83
|
+
seq: number;
|
|
84
|
+
id: string;
|
|
85
|
+
task: string;
|
|
86
|
+
round: number;
|
|
87
|
+
kind: ActionKind;
|
|
88
|
+
changes: string[];
|
|
89
|
+
body: string;
|
|
90
|
+
status: "applying" | "applied" | "undoing" | "undone" | "failed";
|
|
91
|
+
detail: ActionDetail;
|
|
92
|
+
created: number;
|
|
93
|
+
}
|
|
94
|
+
export interface Write {
|
|
95
|
+
action: string;
|
|
96
|
+
phase: "apply" | "undo";
|
|
97
|
+
path: string;
|
|
98
|
+
expected: Buffer | null;
|
|
99
|
+
target: Buffer | null;
|
|
100
|
+
}
|
|
101
|
+
export interface MessageData {
|
|
102
|
+
changes?: string[];
|
|
103
|
+
not_reverted?: string[];
|
|
104
|
+
undid?: string;
|
|
105
|
+
message?: number;
|
|
106
|
+
}
|
|
107
|
+
export interface Message {
|
|
108
|
+
id: number;
|
|
109
|
+
task: string;
|
|
110
|
+
round: number | null;
|
|
111
|
+
kind: ActionKind | "undo" | "note";
|
|
112
|
+
action: string | null;
|
|
113
|
+
body: string;
|
|
114
|
+
data: MessageData;
|
|
115
|
+
created: number;
|
|
116
|
+
}
|
|
117
|
+
export interface JobResult {
|
|
118
|
+
outcome?: "published" | "unchanged" | "empty";
|
|
119
|
+
round?: number | null;
|
|
120
|
+
changes?: number;
|
|
121
|
+
kinds?: Record<string, number>;
|
|
122
|
+
warnings?: string[];
|
|
123
|
+
error?: string;
|
|
124
|
+
details?: string[];
|
|
125
|
+
next?: string;
|
|
126
|
+
log?: string;
|
|
127
|
+
}
|
|
128
|
+
export interface Job {
|
|
129
|
+
id: string;
|
|
130
|
+
task: string;
|
|
131
|
+
status: "queued" | "running" | "done" | "failed";
|
|
132
|
+
phase: string;
|
|
133
|
+
result: JobResult;
|
|
134
|
+
created: number;
|
|
135
|
+
finished?: number;
|
|
136
|
+
}
|
|
137
|
+
export interface BuildInfo {
|
|
138
|
+
key: string;
|
|
139
|
+
tree: string;
|
|
140
|
+
targets: string[];
|
|
141
|
+
root: string;
|
|
142
|
+
cwd: string;
|
|
143
|
+
pdf: string;
|
|
144
|
+
seconds: number;
|
|
145
|
+
warnings: string[];
|
|
146
|
+
}
|
|
147
|
+
export interface Position {
|
|
148
|
+
page: number;
|
|
149
|
+
clip: Rect;
|
|
150
|
+
label: string;
|
|
151
|
+
}
|
|
152
|
+
export interface Mark {
|
|
153
|
+
kind: "removed" | "added" | "moved";
|
|
154
|
+
rect: Rect;
|
|
155
|
+
}
|
|
156
|
+
export interface Pair {
|
|
157
|
+
old: Position | null;
|
|
158
|
+
new: Position | null;
|
|
159
|
+
before_marks: Mark[];
|
|
160
|
+
after_marks: Mark[];
|
|
161
|
+
}
|
|
162
|
+
export type Visual = "unmapped" | "text" | "layout" | "position" | "identical";
|
|
163
|
+
export interface Card {
|
|
164
|
+
id: string;
|
|
165
|
+
file: string;
|
|
166
|
+
pairs: Pair[];
|
|
167
|
+
comparison: Visual;
|
|
168
|
+
own?: { old: SourceBox[]; new: SourceBox[] };
|
|
169
|
+
context_node?: string | null;
|
|
170
|
+
}
|
|
171
|
+
export interface TextGlyph {
|
|
172
|
+
text: string;
|
|
173
|
+
rect: Rect;
|
|
174
|
+
}
|
|
175
|
+
export interface TextLine {
|
|
176
|
+
text: string;
|
|
177
|
+
chars: TextGlyph[];
|
|
178
|
+
}
|
|
179
|
+
export interface Strip {
|
|
180
|
+
src: string;
|
|
181
|
+
clean: string;
|
|
182
|
+
width: number;
|
|
183
|
+
height: number;
|
|
184
|
+
page: number;
|
|
185
|
+
label: string;
|
|
186
|
+
text?: TextLine[];
|
|
187
|
+
}
|
|
188
|
+
export interface ChangeBlock {
|
|
189
|
+
kind: "change";
|
|
190
|
+
units: string[];
|
|
191
|
+
identical: boolean;
|
|
192
|
+
reflow?: boolean;
|
|
193
|
+
before: Strip[];
|
|
194
|
+
after: Strip[];
|
|
195
|
+
}
|
|
196
|
+
export type Block =
|
|
197
|
+
ChangeBlock | { kind: "context"; image: Strip } | { kind: "gap" };
|
|
198
|
+
export interface VisualUnit {
|
|
199
|
+
visual: Visual;
|
|
200
|
+
pages: string[];
|
|
201
|
+
section: string | null;
|
|
202
|
+
}
|
|
203
|
+
export interface RenderedRound {
|
|
204
|
+
units: Record<string, VisualUnit>;
|
|
205
|
+
sections: { node: string; number: string; title: string; blocks: Block[] }[];
|
|
206
|
+
loose: ChangeBlock[];
|
|
207
|
+
}
|
|
208
|
+
export interface RenderRequest {
|
|
209
|
+
main: string;
|
|
210
|
+
source_dir: string;
|
|
211
|
+
figures: string[];
|
|
212
|
+
old: BuildInfo;
|
|
213
|
+
new: BuildInfo;
|
|
214
|
+
units: Unit[];
|
|
215
|
+
}
|
|
216
|
+
export interface History {
|
|
217
|
+
round: number;
|
|
218
|
+
change: string;
|
|
219
|
+
comment: string;
|
|
220
|
+
new_text: string;
|
|
221
|
+
}
|
|
222
|
+
export interface RoundDetail extends Omit<RenderedRound, "units"> {
|
|
223
|
+
task: string;
|
|
224
|
+
round: number;
|
|
225
|
+
before: string;
|
|
226
|
+
after: string;
|
|
227
|
+
units: (Unit & Partial<VisualUnit> & { history?: History[] })[];
|
|
228
|
+
warnings: string[];
|
|
229
|
+
files: string;
|
|
230
|
+
}
|
|
231
|
+
export type TaskState =
|
|
232
|
+
"editing" | "building" | "build_failed" | "reviewing" | "revise" | "done";
|
|
233
|
+
export interface Summary {
|
|
234
|
+
task: string;
|
|
235
|
+
title: string;
|
|
236
|
+
state: TaskState;
|
|
237
|
+
round: number | null;
|
|
238
|
+
counts: Record<Decision, number>;
|
|
239
|
+
next: string;
|
|
240
|
+
}
|
|
241
|
+
export interface PageState {
|
|
242
|
+
csrf: string;
|
|
243
|
+
task: string;
|
|
244
|
+
title: string;
|
|
245
|
+
closed: boolean;
|
|
246
|
+
/** Optional host-provided reason that file decisions or comments are unavailable. */
|
|
247
|
+
decisionLock?: string | null;
|
|
248
|
+
commentLock?: string | null;
|
|
249
|
+
state: TaskState | null;
|
|
250
|
+
latest: number | null;
|
|
251
|
+
round: number | null;
|
|
252
|
+
revision: string | null;
|
|
253
|
+
decisions: Record<string, Decision>;
|
|
254
|
+
comments: Record<string, string>;
|
|
255
|
+
undo: { id: string; kind: ActionKind; changes: string[] } | null;
|
|
256
|
+
job:
|
|
257
|
+
| (Pick<Job, "id" | "status" | "phase" | "created"> &
|
|
258
|
+
JobResult & { ahead: number })
|
|
259
|
+
| null;
|
|
260
|
+
}
|
|
261
|
+
export interface FeedbackItem {
|
|
262
|
+
id: number;
|
|
263
|
+
kind: Message["kind"];
|
|
264
|
+
round: number | null;
|
|
265
|
+
text?: string;
|
|
266
|
+
changes?:
|
|
267
|
+
| string[]
|
|
268
|
+
| { id: string; file: string; lines: Span; old: string; new: string }[];
|
|
269
|
+
not_reverted?: string[];
|
|
270
|
+
undid?: string;
|
|
271
|
+
}
|
|
272
|
+
export interface Feedback extends Summary {
|
|
273
|
+
feedback: FeedbackItem[];
|
|
274
|
+
cursor: number;
|
|
275
|
+
build_error?: JobResult;
|
|
276
|
+
warnings?: string[];
|
|
277
|
+
}
|
|
278
|
+
export interface TaskInfo extends Summary {
|
|
279
|
+
worktree: string;
|
|
280
|
+
url: string;
|
|
281
|
+
viewing: boolean;
|
|
282
|
+
scope: { sections: string[]; files: string[] };
|
|
283
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import type { Decision } from "../types.ts";
|
|
3
|
+
import { stopState, type Stop } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
type Region = { top: number; height: number };
|
|
6
|
+
|
|
7
|
+
export function ChangeMap({
|
|
8
|
+
scrollRoot,
|
|
9
|
+
stops,
|
|
10
|
+
locations,
|
|
11
|
+
decisions,
|
|
12
|
+
current,
|
|
13
|
+
disabled,
|
|
14
|
+
onSelect,
|
|
15
|
+
}: {
|
|
16
|
+
scrollRoot: RefObject<HTMLElement | null>;
|
|
17
|
+
stops: Stop[];
|
|
18
|
+
locations: string[];
|
|
19
|
+
decisions: Record<string, Decision>;
|
|
20
|
+
current: number;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
onSelect: (index: number) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const rail = useRef<HTMLElement>(null),
|
|
25
|
+
drag = useRef<{ pointer: number; offset: number } | null>(null),
|
|
26
|
+
[regions, setRegions] = useState<Region[]>([]),
|
|
27
|
+
[viewport, setViewport] = useState<Region>({ top: 0, height: 0 });
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const root = scrollRoot.current,
|
|
31
|
+
track = rail.current;
|
|
32
|
+
if (!root || !track) return;
|
|
33
|
+
let frame = 0,
|
|
34
|
+
needsMeasure = true;
|
|
35
|
+
const update = () => {
|
|
36
|
+
frame = 0;
|
|
37
|
+
const extent = Math.max(root.scrollHeight, 1),
|
|
38
|
+
trackHeight = track.clientHeight,
|
|
39
|
+
maxScroll = Math.max(0, extent - root.clientHeight),
|
|
40
|
+
thumbHeight = Math.min(
|
|
41
|
+
trackHeight,
|
|
42
|
+
Math.max(8, (root.clientHeight / extent) * trackHeight),
|
|
43
|
+
);
|
|
44
|
+
setViewport({
|
|
45
|
+
top: maxScroll
|
|
46
|
+
? (Math.max(0, Math.min(root.scrollTop, maxScroll)) / maxScroll) *
|
|
47
|
+
(trackHeight - thumbHeight)
|
|
48
|
+
: 0,
|
|
49
|
+
height: thumbHeight,
|
|
50
|
+
});
|
|
51
|
+
if (!needsMeasure) return;
|
|
52
|
+
needsMeasure = false;
|
|
53
|
+
const origin = root.getBoundingClientRect().top + root.clientTop;
|
|
54
|
+
setRegions(
|
|
55
|
+
[...root.querySelectorAll<HTMLElement>("[data-stop]")].map(
|
|
56
|
+
(article) => {
|
|
57
|
+
const box = article.getBoundingClientRect(),
|
|
58
|
+
top = Math.max(
|
|
59
|
+
0,
|
|
60
|
+
((box.top - origin + root.scrollTop) / extent) * trackHeight,
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
top,
|
|
64
|
+
height: Math.max(
|
|
65
|
+
0,
|
|
66
|
+
Math.min(
|
|
67
|
+
trackHeight - top,
|
|
68
|
+
Math.max(4, (box.height / extent) * trackHeight),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
const schedule = () => {
|
|
77
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
78
|
+
};
|
|
79
|
+
const observer = new ResizeObserver(() => {
|
|
80
|
+
needsMeasure = true;
|
|
81
|
+
schedule();
|
|
82
|
+
});
|
|
83
|
+
observer.observe(root);
|
|
84
|
+
observer.observe(track);
|
|
85
|
+
for (const child of root.children) observer.observe(child);
|
|
86
|
+
for (const article of root.querySelectorAll("[data-stop]"))
|
|
87
|
+
observer.observe(article);
|
|
88
|
+
// Banners can move the document without changing its own dimensions.
|
|
89
|
+
const mutations = new MutationObserver((records) => {
|
|
90
|
+
for (const record of records) {
|
|
91
|
+
for (const child of record.removedNodes)
|
|
92
|
+
if (child instanceof Element) observer.unobserve(child);
|
|
93
|
+
for (const child of record.addedNodes)
|
|
94
|
+
if (child instanceof Element) observer.observe(child);
|
|
95
|
+
}
|
|
96
|
+
needsMeasure = true;
|
|
97
|
+
schedule();
|
|
98
|
+
});
|
|
99
|
+
mutations.observe(root, { childList: true });
|
|
100
|
+
root.addEventListener("scroll", schedule, { passive: true });
|
|
101
|
+
schedule();
|
|
102
|
+
return () => {
|
|
103
|
+
observer.disconnect();
|
|
104
|
+
mutations.disconnect();
|
|
105
|
+
root.removeEventListener("scroll", schedule);
|
|
106
|
+
cancelAnimationFrame(frame);
|
|
107
|
+
};
|
|
108
|
+
}, [scrollRoot, stops]);
|
|
109
|
+
|
|
110
|
+
if (!stops.length) return null;
|
|
111
|
+
|
|
112
|
+
function scrollAt(clientY: number, offset: number) {
|
|
113
|
+
const root = scrollRoot.current,
|
|
114
|
+
track = rail.current;
|
|
115
|
+
if (!root || !track) return;
|
|
116
|
+
const y = clientY - track.getBoundingClientRect().top - track.clientTop,
|
|
117
|
+
travel = Math.max(1, track.clientHeight - viewport.height);
|
|
118
|
+
root.scrollTo({
|
|
119
|
+
top: ((y - offset) / travel) * (root.scrollHeight - root.clientHeight),
|
|
120
|
+
behavior: "instant",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<nav
|
|
126
|
+
ref={rail}
|
|
127
|
+
className="change-map"
|
|
128
|
+
aria-label="Change map"
|
|
129
|
+
onPointerDown={(event) => {
|
|
130
|
+
if (
|
|
131
|
+
event.button !== 0 ||
|
|
132
|
+
(event.target as HTMLElement).closest("button")
|
|
133
|
+
)
|
|
134
|
+
return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const offset = (event.target as HTMLElement).classList.contains(
|
|
137
|
+
"change-map-viewport",
|
|
138
|
+
)
|
|
139
|
+
? event.clientY -
|
|
140
|
+
event.currentTarget.getBoundingClientRect().top -
|
|
141
|
+
event.currentTarget.clientTop -
|
|
142
|
+
viewport.top
|
|
143
|
+
: viewport.height / 2;
|
|
144
|
+
drag.current = { pointer: event.pointerId, offset };
|
|
145
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
146
|
+
scrollAt(event.clientY, offset);
|
|
147
|
+
}}
|
|
148
|
+
onPointerMove={(event) => {
|
|
149
|
+
if (drag.current?.pointer === event.pointerId)
|
|
150
|
+
scrollAt(event.clientY, drag.current.offset);
|
|
151
|
+
}}
|
|
152
|
+
onPointerUp={(event) => {
|
|
153
|
+
if (drag.current?.pointer === event.pointerId) {
|
|
154
|
+
drag.current = null;
|
|
155
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
onLostPointerCapture={() => {
|
|
159
|
+
drag.current = null;
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<div
|
|
163
|
+
className="change-map-viewport"
|
|
164
|
+
style={{ top: viewport.top, height: viewport.height }}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
/>
|
|
167
|
+
{stops.map((stop, index) => {
|
|
168
|
+
const region = regions[index];
|
|
169
|
+
if (!region) return null;
|
|
170
|
+
const state = stopState(stop.units, decisions),
|
|
171
|
+
label = `Change ${index + 1} · ${state} · ${locations[index]}`;
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
key={stop.key}
|
|
175
|
+
type="button"
|
|
176
|
+
className="change-map-mark"
|
|
177
|
+
data-stop-index={index}
|
|
178
|
+
data-state={state}
|
|
179
|
+
aria-label={label}
|
|
180
|
+
aria-current={index === current ? "location" : undefined}
|
|
181
|
+
tabIndex={index === Math.max(0, current) ? 0 : -1}
|
|
182
|
+
disabled={disabled}
|
|
183
|
+
style={{ top: region.top, height: region.height }}
|
|
184
|
+
onClick={() => onSelect(index)}
|
|
185
|
+
onKeyDown={(event) => {
|
|
186
|
+
const target =
|
|
187
|
+
event.key === "ArrowDown"
|
|
188
|
+
? index + 1
|
|
189
|
+
: event.key === "ArrowUp"
|
|
190
|
+
? index - 1
|
|
191
|
+
: event.key === "Home"
|
|
192
|
+
? 0
|
|
193
|
+
: event.key === "End"
|
|
194
|
+
? stops.length - 1
|
|
195
|
+
: null;
|
|
196
|
+
if (target === null) return;
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
rail.current
|
|
200
|
+
?.querySelector<HTMLButtonElement>(
|
|
201
|
+
`[data-stop-index="${Math.max(0, Math.min(stops.length - 1, target))}"]`,
|
|
202
|
+
)
|
|
203
|
+
?.focus();
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<span className="change-map-tooltip">{label}</span>
|
|
207
|
+
</button>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
</nav>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { memo, useMemo, type CSSProperties } from "react";
|
|
2
|
+
import type { Strip, TextGlyph } from "../types.ts";
|
|
3
|
+
let context: CanvasRenderingContext2D | null | undefined;
|
|
4
|
+
const widths = new Map<string, number>();
|
|
5
|
+
function width(text: string) {
|
|
6
|
+
if (widths.has(text)) return widths.get(text)!;
|
|
7
|
+
context ??= document.createElement("canvas").getContext("2d");
|
|
8
|
+
if (!context) return 1;
|
|
9
|
+
context.font = '100px "Times New Roman", serif';
|
|
10
|
+
const measured = context.measureText(text).width / 100;
|
|
11
|
+
widths.set(text, measured);
|
|
12
|
+
return measured || 1;
|
|
13
|
+
}
|
|
14
|
+
function style(glyph: TextGlyph, image: Strip): CSSProperties {
|
|
15
|
+
const [x0, y0, x1, y1] = glyph.rect,
|
|
16
|
+
height = Math.max(y1 - y0, 0.1),
|
|
17
|
+
scaleX = (x1 - x0) / (height * width(glyph.text));
|
|
18
|
+
return {
|
|
19
|
+
left: `${(100 * x0) / image.width}%`,
|
|
20
|
+
top: `${(100 * y0) / image.height}%`,
|
|
21
|
+
fontSize: `${(100 * height) / image.width}cqw`,
|
|
22
|
+
transform: `scaleX(${scaleX})`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const PdfStrip = memo(function PdfStrip({
|
|
26
|
+
image,
|
|
27
|
+
base,
|
|
28
|
+
label,
|
|
29
|
+
clean = false,
|
|
30
|
+
}: {
|
|
31
|
+
image: Strip;
|
|
32
|
+
base: string;
|
|
33
|
+
label: string;
|
|
34
|
+
clean?: boolean;
|
|
35
|
+
}) {
|
|
36
|
+
const text = useMemo(
|
|
37
|
+
() =>
|
|
38
|
+
image.text?.map((line, i) => (
|
|
39
|
+
<span className="pdf-text-line" key={i}>
|
|
40
|
+
{line.chars.map((glyph, j) => (
|
|
41
|
+
<span key={j} className="pdf-glyph" style={style(glyph, image)}>
|
|
42
|
+
{glyph.text}
|
|
43
|
+
</span>
|
|
44
|
+
))}
|
|
45
|
+
{"\n"}
|
|
46
|
+
</span>
|
|
47
|
+
)),
|
|
48
|
+
[image],
|
|
49
|
+
);
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className="pdf-strip"
|
|
53
|
+
data-page={image.label}
|
|
54
|
+
style={{ aspectRatio: `${image.width}/${image.height}` }}
|
|
55
|
+
>
|
|
56
|
+
<img
|
|
57
|
+
src={base + (clean ? image.clean : image.src)}
|
|
58
|
+
alt={image.text?.length ? "" : label}
|
|
59
|
+
width={image.width}
|
|
60
|
+
height={image.height}
|
|
61
|
+
loading="lazy"
|
|
62
|
+
decoding="async"
|
|
63
|
+
draggable={false}
|
|
64
|
+
aria-hidden={Boolean(image.text?.length)}
|
|
65
|
+
/>
|
|
66
|
+
{image.text?.length ? (
|
|
67
|
+
<div
|
|
68
|
+
className="pdf-text"
|
|
69
|
+
role="region"
|
|
70
|
+
aria-label={`${label}, PDF page ${image.label}`}
|
|
71
|
+
>
|
|
72
|
+
{text}
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
% This is a preliminary version (2006-09-30), barring acceptance from
|
|
2
|
+
% the LaTeX Project Team and other feedback, of the GUST Font License.
|
|
3
|
+
% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
|
|
4
|
+
%
|
|
5
|
+
% For the most recent version of this license see
|
|
6
|
+
% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
|
|
7
|
+
% or
|
|
8
|
+
% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
|
|
9
|
+
%
|
|
10
|
+
% This work may be distributed and/or modified under the conditions
|
|
11
|
+
% of the LaTeX Project Public License, either version 1.3c of this
|
|
12
|
+
% license or (at your option) any later version.
|
|
13
|
+
%
|
|
14
|
+
% Please also observe the following clause:
|
|
15
|
+
% 1) it is requested, but not legally required, that derived works be
|
|
16
|
+
% distributed only after changing the names of the fonts comprising this
|
|
17
|
+
% work and given in an accompanying "manifest", and that the
|
|
18
|
+
% files comprising the Work, as listed in the manifest, also be given
|
|
19
|
+
% new names. Any exceptions to this request are also given in the
|
|
20
|
+
% manifest.
|
|
21
|
+
%
|
|
22
|
+
% We recommend the manifest be given in a separate file named
|
|
23
|
+
% MANIFEST-<fontid>.txt, where <fontid> is some unique identification
|
|
24
|
+
% of the font family. If a separate "readme" file accompanies the Work,
|
|
25
|
+
% we recommend a name of the form README-<fontid>.txt.
|
|
26
|
+
%
|
|
27
|
+
% The latest version of the LaTeX Project Public License is in
|
|
28
|
+
% http://www.latex-project.org/lppl.txt and version 1.3c or later
|
|
29
|
+
% is part of all distributions of LaTeX version 2006/05/20 or later.
|