@embedpdf/viewer-chrome 3.0.0-next.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/dist/index.js ADDED
@@ -0,0 +1,3529 @@
1
+ import { defaultChrome, getMenu, getModeBar, getStrip } from "./chrome.js";
2
+ import { TOOL_ICONS, defaultCommands } from "./commands.js";
3
+ import { Fragment, createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { DocumentGate, DocumentsToken, Viewer, createCapabilityToken, definePlugin, useCapability, useDocumentId, useDocumentStatus, useDocuments, useKernel, useOptionalCapability, useOptionalSelector, useSelector } from "@embedpdf/react/runtime";
5
+ import { Stage, StageToken, StageToken as StageToken$1, stagePlugin, usePages } from "@embedpdf/react/stage";
6
+ import { RenderLayer, TileLayer, renderPlugin } from "@embedpdf/react/render";
7
+ import { pageEditPlugin } from "@embedpdf/react/page-edit";
8
+ import { InteractionToken, InteractionToken as InteractionToken$1, interactionPlugin, useTool, useToolCursor } from "@embedpdf/react/interaction";
9
+ import { SelectionLayer, SelectionToken, selectionPlugin } from "@embedpdf/react/selection";
10
+ import { AnnotationLayer, AnnotationToken, AnnotationToken as AnnotationToken$1, annotationPlugin, useAnnotation, useAnnotationDefaults, useAnnotationSelected, useFilePickerProvider, useSelectionFlags, useSelectionProps } from "@embedpdf/react/annotation";
11
+ import { RedactionToken, RedactionToken as RedactionToken$1, redactionPlugin, usePendingRedactions, useRedaction } from "@embedpdf/react/redaction";
12
+ import { FormToken, formPlugin, formWidgetRenderer, useFormScriptingProvider } from "@embedpdf/react/form";
13
+ import { LinkLayer, LinkToken, linkPlugin } from "@embedpdf/react/link";
14
+ import { SearchLayer, SearchToken, SearchToken as SearchToken$1, searchPlugin, useSearch, useSearchState, validateSearchQuery } from "@embedpdf/react/search";
15
+ import { I18nToken, i18nPlugin, negotiateLocale, useT } from "@embedpdf/react/i18n";
16
+ import { CommandsToken, CommandsToken as CommandsToken$1, commandsPlugin, resolvedCommandsEqual, useCommand, useCommandShortcuts, useCommands } from "@embedpdf/react/commands";
17
+ import { ShellToken, ShellToken as ShellToken$1, shellPlugin, useMenus, useSurface } from "@embedpdf/react/shell";
18
+ import { Toolbar, addItem, chromeHelpers, chromeHelpers as chromeHelpers$1, custom, defineChrome, group, item, removeItems, replaceItem, useStripView, validateChrome, validateChrome as validateChrome$1 } from "@embedpdf/react/toolbar";
19
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
20
+ import { Scrollbar } from "@embedpdf/react/scrollbar";
21
+ import { AnnotationMenu } from "@embedpdf/react/annotation-menu";
22
+ import { MetadataToken } from "@embedpdf/react/metadata";
23
+ //#region src/config/stage.ts
24
+ /**
25
+ * The Stage is a LENS: one document can be viewed through several at once. The
26
+ * thumbnail sidebar is a second lens over the SAME document — a single-column,
27
+ * fixed-zoom grid with its own camera, fully independent of the main view.
28
+ *
29
+ * The token lives here (not in App.tsx) because two places need it: App wires the
30
+ * `stagePlugin` lens with it, and the sidebar renders `<Stage token={...}>` off it.
31
+ */
32
+ const ThumbsStageToken = createCapabilityToken("stage-thumbs");
33
+ //#endregion
34
+ //#region src/config/demo-tools.plugin.ts
35
+ /**
36
+ * Example-only: register the insert / redact authoring tools as INERT
37
+ * interaction tools. v3 doesn't yet ship signature or redaction plugins, but
38
+ * this is a CHROME demo — its job is selection state and layout, not the
39
+ * tool's effect. Registering them as real (behaviourless) tools lets every
40
+ * tool button in every mode band go through the SAME path
41
+ * (`InteractionToken.activateTool`), so `active` is uniform and honest: the
42
+ * button really is the active tool; it just has no handler behind it.
43
+ *
44
+ * The annotate + shapes tools are NOT here — those are real tools the
45
+ * annotation plugin registers (highlight, ink, square, circle, …) — and
46
+ * neither are the form palette tools (the form plugin's draw-to-place).
47
+ */
48
+ const INERT_TOOLS = [{
49
+ id: "signature",
50
+ cursor: "copy"
51
+ }, {
52
+ id: "image",
53
+ cursor: "copy"
54
+ }];
55
+ const demoToolsPlugin = () => definePlugin({
56
+ id: "demo-tools",
57
+ scope: "document",
58
+ requires: [InteractionToken$1],
59
+ init: (ctx) => {
60
+ const interaction = ctx.get(InteractionToken$1);
61
+ for (const tool of INERT_TOOLS) interaction.registerTool({
62
+ id: tool.id,
63
+ cursor: tool.cursor,
64
+ enables: /* @__PURE__ */ new Set()
65
+ });
66
+ }
67
+ });
68
+ //#endregion
69
+ //#region src/locales/en.ts
70
+ /**
71
+ * English pack. Strings belong to the PRODUCT, not the plugins — the
72
+ * `commands.*` namespace mirrors the v2 snippet packs so translations carry
73
+ * over; `demo.*` is this example's own chrome.
74
+ */
75
+ const en = {
76
+ code: "en",
77
+ name: "English",
78
+ translations: {
79
+ commands: {
80
+ zoom: {
81
+ in: "Zoom In",
82
+ out: "Zoom Out",
83
+ fitPage: "Fit to Page",
84
+ fitWidth: "Fit to Width",
85
+ automatic: "Automatic",
86
+ menu: "Zoom",
87
+ level: "Zoom Level",
88
+ p50: "50%",
89
+ p100: "100%",
90
+ p150: "150%",
91
+ p200: "200%",
92
+ p400: "400%"
93
+ },
94
+ pan: "Pan",
95
+ pointer: "Pointer",
96
+ sidebar: "Sidebar",
97
+ search: "Search",
98
+ comment: "Comment",
99
+ style: "Style",
100
+ menu: "Menu",
101
+ viewControls: "View Controls",
102
+ download: "Download",
103
+ print: "Print",
104
+ fullscreen: "Full Screen",
105
+ spread: {
106
+ group: "Page Layout",
107
+ none: "Single Page",
108
+ odd: "Two Page",
109
+ even: "Cover Page"
110
+ },
111
+ scroll: {
112
+ group: "Scrolling",
113
+ vertical: "Vertical",
114
+ horizontal: "Horizontal"
115
+ },
116
+ rotate: {
117
+ group: "Rotate",
118
+ clockwise: "Rotate Clockwise",
119
+ counterclockwise: "Rotate Counter-Clockwise"
120
+ },
121
+ mode: {
122
+ group: "Mode",
123
+ view: "View",
124
+ annotate: "Annotate",
125
+ shapes: "Shapes",
126
+ insert: "Insert",
127
+ form: "Form",
128
+ redact: "Redact"
129
+ },
130
+ annotate: {
131
+ highlight: "Highlight",
132
+ strikeout: "Strikeout",
133
+ underline: "Underline",
134
+ squiggly: "Squiggly",
135
+ ink: "Draw",
136
+ inkHighlight: "Ink Highlighter",
137
+ text: "Text",
138
+ insertText: "Insert Text",
139
+ replaceText: "Replace Text",
140
+ callout: "Callout",
141
+ note: "Comment",
142
+ link: "Link",
143
+ gotoLink: "Go to link",
144
+ removeLink: "Remove link",
145
+ delete: "Delete",
146
+ group: "Group",
147
+ ungroup: "Ungroup",
148
+ strip: "Annotation actions"
149
+ },
150
+ shapes: {
151
+ rectangle: "Rectangle",
152
+ circle: "Circle",
153
+ line: "Line",
154
+ arrow: "Arrow",
155
+ polygon: "Polygon",
156
+ polyline: "Polyline"
157
+ },
158
+ insert: {
159
+ stamp: "Rubber Stamp",
160
+ attachment: "Attach File",
161
+ signature: "Signature",
162
+ image: "Image"
163
+ },
164
+ form: {
165
+ textfield: "Text Field",
166
+ checkbox: "Checkbox",
167
+ radio: "Radio Button",
168
+ select: "Dropdown",
169
+ listbox: "List Box"
170
+ },
171
+ redact: {
172
+ mark: "Mark for Redaction",
173
+ applyAll: "Apply Redactions",
174
+ clearAll: "Clear Marks",
175
+ panel: "Redactions"
176
+ },
177
+ undo: "Undo",
178
+ redo: "Redo"
179
+ },
180
+ passwordPrompt: {
181
+ title: "Password required",
182
+ required: "This document is password protected. Enter the password to open it.",
183
+ incorrect: "The password was incorrect. Please try again.",
184
+ incorrectWarning: "Incorrect password",
185
+ label: "Password",
186
+ placeholder: "Enter password",
187
+ cancel: "Cancel",
188
+ open: "Open",
189
+ opening: "Opening…"
190
+ },
191
+ documentError: {
192
+ title: "Couldn’t open document",
193
+ unknown: "The document could not be opened.",
194
+ close: "Close document"
195
+ },
196
+ demo: {
197
+ starting: "Starting viewer…",
198
+ opening: "Opening document…",
199
+ more: "More",
200
+ thumbnails: "Thumbnails",
201
+ outline: "Outline",
202
+ searchPlaceholder: "Search document…",
203
+ searchTitle: "Search",
204
+ searchCaseSensitive: "Case sensitive",
205
+ searchWholeWord: "Whole word",
206
+ searchRegex: "Regular expression",
207
+ searchSearching: "Searching…",
208
+ searchResults: "{count} results found",
209
+ searchNoResults: "No results",
210
+ searchPage: "Page {page}",
211
+ commentsTitle: "Comments",
212
+ styleTitle: "Style",
213
+ empty: "Nothing here yet in this demo panel.",
214
+ page: "Page {current} of {total}",
215
+ cancel: "Cancel",
216
+ pageBadge: "p. {page}",
217
+ redactionTitle: "Redactions",
218
+ redactionEmpty: "No pending redactions. Use the Redact tool to mark text or areas.",
219
+ redactionArea: "Area mark",
220
+ redactionText: "Text mark",
221
+ redactionRemove: "Remove mark",
222
+ redactConfirmTitle: "Apply redactions?",
223
+ redactConfirmBody: "Permanently remove the content under {count} redaction marks. This cannot be undone.",
224
+ redactConfirmCollateral: "{count} other annotations will also be removed.",
225
+ redactApply: "Apply",
226
+ redactApplying: "Applying…",
227
+ redactLabelTitle: "Redaction label",
228
+ redactLabelPlaceholder: "e.g. REDACTED",
229
+ redactLabelRepeat: "Repeat label across region"
230
+ }
231
+ }
232
+ };
233
+ //#endregion
234
+ //#region src/config-context.tsx
235
+ /**
236
+ * The resolved customization, as React context. The chrome schema and the
237
+ * user's extra icons are RESOLVED ONCE in <FullViewer> and read here by the
238
+ * shell, menus, and strips — no component imports the default schema value
239
+ * directly, so "the host replaced the chrome" is invisible below this line.
240
+ *
241
+ * The default context value is the default config: components render correctly
242
+ * in tests and stories without a provider.
243
+ */
244
+ const ViewerConfigContext = createContext({
245
+ chrome: defaultChrome,
246
+ icons: {}
247
+ });
248
+ const ViewerConfigProvider = ViewerConfigContext.Provider;
249
+ function useChromeSchema() {
250
+ return useContext(ViewerConfigContext).chrome;
251
+ }
252
+ function useMenuSchema(id) {
253
+ return getMenu(useContext(ViewerConfigContext).chrome, id);
254
+ }
255
+ function useStripSchema(id) {
256
+ return getStrip(useContext(ViewerConfigContext).chrome, id);
257
+ }
258
+ function useCustomIcons() {
259
+ return useContext(ViewerConfigContext).icons;
260
+ }
261
+ //#endregion
262
+ //#region src/handle.ts
263
+ function createViewerHandle(kernel) {
264
+ const watch = (select, cb, isEqual = Object.is) => {
265
+ let previous = select();
266
+ return kernel.subscribe(() => {
267
+ const next = select();
268
+ if (isEqual(previous, next)) return;
269
+ const before = previous;
270
+ previous = next;
271
+ cb(next, before);
272
+ });
273
+ };
274
+ const commands = () => kernel.capability(CommandsToken$1);
275
+ return {
276
+ documents: kernel.documents,
277
+ get: (token) => kernel.capability(token),
278
+ tryGet: (token) => kernel.tryCapability(token),
279
+ forDocument: (documentId) => ({
280
+ get: (token) => kernel.capability(token, documentId),
281
+ tryGet: (token) => kernel.tryCapability(token, documentId)
282
+ }),
283
+ watch,
284
+ execute: (id, documentId) => commands().execute(id, documentId),
285
+ resolve: (id, documentId) => commands().resolve(id, documentId) ?? null,
286
+ watchCommand: (id, cb) => watch(() => commands().resolve(id) ?? null, cb, resolvedCommandsEqual)
287
+ };
288
+ }
289
+ //#endregion
290
+ //#region src/ui/icons.tsx
291
+ const ICON_PATHS = {
292
+ alertTriangle: ["M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"],
293
+ alignBottom: [
294
+ "M3 19v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
295
+ "M11 17h2",
296
+ "M9 14h6",
297
+ "M10 11h4"
298
+ ],
299
+ alignCenter: [
300
+ "M4 6l16 0",
301
+ "M8 12l8 0",
302
+ "M6 18l12 0"
303
+ ],
304
+ alignLeft: [
305
+ "M4 6l16 0",
306
+ "M4 12l10 0",
307
+ "M4 18l14 0"
308
+ ],
309
+ alignMiddle: [
310
+ "M3 19v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
311
+ "M11 15h2",
312
+ "M9 12h6",
313
+ "M10 9h4"
314
+ ],
315
+ alignRight: [
316
+ "M4 6l16 0",
317
+ "M10 12l10 0",
318
+ "M6 18l14 0"
319
+ ],
320
+ alignTop: [
321
+ "M3 19v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
322
+ "M11 13h2",
323
+ "M9 10h6",
324
+ "M10 7h4"
325
+ ],
326
+ arrowBackUp: ["M9 14l-4 -4l4 -4", "M5 10h11a4 4 0 1 1 0 8h-1"],
327
+ arrowForwardUp: ["M15 14l4 -4l-4 -4", "M19 10h-11a4 4 0 1 0 0 8h1"],
328
+ bold: ["M7 5h6a3.5 3.5 0 0 1 0 7h-6z", "M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7"],
329
+ book: [
330
+ "M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0",
331
+ "M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0",
332
+ "M3 6l0 13",
333
+ "M12 6l0 13",
334
+ "M21 6l0 13"
335
+ ],
336
+ book2: [
337
+ "M19 4v16h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12z",
338
+ "M19 16h-12a2 2 0 0 0 -2 2",
339
+ "M9 8h6"
340
+ ],
341
+ callout: [
342
+ {
343
+ d: "M20 6v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1",
344
+ stroke: "primary",
345
+ fill: "secondary"
346
+ },
347
+ {
348
+ d: "M17 11v6h-9",
349
+ stroke: "primary"
350
+ },
351
+ {
352
+ d: "M8 15l-2 2l2 2",
353
+ stroke: "primary"
354
+ }
355
+ ],
356
+ check: ["M5 12l5 5l10 -10"],
357
+ chevronDown: ["M6 9l6 6l6 -6"],
358
+ chevronLeft: ["M15 6l-6 6l6 6"],
359
+ chevronRight: ["M9 6l6 6l-6 6"],
360
+ circle: [{
361
+ d: "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0",
362
+ stroke: "primary",
363
+ fill: "secondary"
364
+ }],
365
+ comment: [
366
+ "M12 11v.01",
367
+ "M8 11v.01",
368
+ "M16 11v.01",
369
+ "M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3z"
370
+ ],
371
+ copy: ["M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z", "M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1"],
372
+ deviceFloppy: [
373
+ "M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2",
374
+ "M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
375
+ "M14 4l0 4l-6 0l0 -4"
376
+ ],
377
+ dots: [
378
+ "M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
379
+ "M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
380
+ "M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"
381
+ ],
382
+ doublePage: ["M3 3m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1zm9 -1v18"],
383
+ download: [
384
+ "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2",
385
+ "M7 11l5 5l5 -5",
386
+ "M12 4l0 12"
387
+ ],
388
+ externalLink: [
389
+ "M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6",
390
+ "M11 13l9 -9",
391
+ "M15 4h5v5"
392
+ ],
393
+ eye: ["M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"],
394
+ file: ["M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"],
395
+ fileImport: ["M14 3v4a1 1 0 0 0 1 1h4", "M5 13v-8a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-5.5m-9.5 -2h7m-3 -3l3 3l-3 3"],
396
+ fitToPage: [
397
+ "M12 20h-6a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h6",
398
+ "M18 14v7",
399
+ "M18 3v7",
400
+ "M15 18l3 3l3 -3",
401
+ "M15 6l3 -3l3 3"
402
+ ],
403
+ fitToWidth: [
404
+ "M4 12v-6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6",
405
+ "M10 18h-7",
406
+ "M21 18h-7",
407
+ "M6 15l-3 3l3 3",
408
+ "M18 15l3 3l-3 3"
409
+ ],
410
+ formCheckbox: ["M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14", "M9 12l2 2l4 -4"],
411
+ formListbox: [
412
+ "M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14",
413
+ "M11 8h6",
414
+ "M11 12h6",
415
+ "M11 16h6",
416
+ "M7 8l0 .01",
417
+ "M7 12l0 .01",
418
+ "M7 16l0 .01"
419
+ ],
420
+ formRadio: [{
421
+ d: "M9 12a2 2 0 1 0 6 0a2 2 0 1 0 -6 0",
422
+ fill: true
423
+ }, "M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"],
424
+ formSelect: ["M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14", "M9 11l3 3l3 -3"],
425
+ formTextfield: [
426
+ "M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3",
427
+ "M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3",
428
+ "M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7",
429
+ "M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1",
430
+ "M17 12h.01",
431
+ "M13 12h.01"
432
+ ],
433
+ freeText: [
434
+ {
435
+ d: "M4 7v-2h12v2",
436
+ stroke: "primary"
437
+ },
438
+ {
439
+ d: "M10 5v14",
440
+ stroke: "primary"
441
+ },
442
+ {
443
+ d: "M12 19h-4",
444
+ stroke: "primary"
445
+ },
446
+ {
447
+ d: "M15 13v-1h6v1",
448
+ stroke: "primary"
449
+ },
450
+ {
451
+ d: "M18 12v7",
452
+ stroke: "primary"
453
+ },
454
+ {
455
+ d: "M17 19h2",
456
+ stroke: "primary"
457
+ }
458
+ ],
459
+ fullscreen: [
460
+ "M4 8v-2a2 2 0 0 1 2 -2h2",
461
+ "M4 16v2a2 2 0 0 0 2 2h2",
462
+ "M16 4h2a2 2 0 0 1 2 2v2",
463
+ "M16 20h2a2 2 0 0 0 2 -2v-2"
464
+ ],
465
+ fullscreenExit: [
466
+ "M4 8v-2c0 -.551 .223 -1.05 .584 -1.412",
467
+ "M4 16v2a2 2 0 0 0 2 2h2",
468
+ "M16 4h2a2 2 0 0 1 2 2v2",
469
+ "M16 20h2c.545 0 1.04 -.218 1.4 -.572",
470
+ "M3 3l18 18"
471
+ ],
472
+ group: [
473
+ "M3 5a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
474
+ "M17 5a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
475
+ "M3 19a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
476
+ "M17 19a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
477
+ "M5 7v10",
478
+ "M19 7v10",
479
+ "M7 5h10",
480
+ "M7 19h10",
481
+ "M8 8H12.8V11.2H16V16H11.2V12.8H8V8Z"
482
+ ],
483
+ hand: [
484
+ "M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5",
485
+ "M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5",
486
+ "M14 5.5a1.5 1.5 0 0 1 3 0v6.5",
487
+ "M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"
488
+ ],
489
+ highlight: [
490
+ {
491
+ d: "M4 6h16a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2H4a2 2 0 0 1 -2 -2V8a2 2 0 0 1 2 -2z",
492
+ fill: "primary"
493
+ },
494
+ "M8 16v-8a4 4 0 1 1 8 0v8",
495
+ "M8 10h8"
496
+ ],
497
+ horizontal: [
498
+ "M7 8l-4 4l4 4",
499
+ "M17 8l4 4l-4 4",
500
+ "M3 12l18 0"
501
+ ],
502
+ inkHighlighter: [
503
+ "M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4",
504
+ "M12.5 5.5l4 4",
505
+ "M4.5 13.5l4 4",
506
+ {
507
+ d: "M21 15v4h-8l4 -4l4 0",
508
+ stroke: "primary",
509
+ fill: "primary"
510
+ }
511
+ ],
512
+ insertText: [
513
+ {
514
+ d: "M3 7v-2h12v2",
515
+ stroke: "primary"
516
+ },
517
+ {
518
+ d: "M9 5v14",
519
+ stroke: "primary"
520
+ },
521
+ {
522
+ d: "M7 19h4",
523
+ stroke: "primary"
524
+ },
525
+ {
526
+ d: "M18 15l2 2h-5",
527
+ stroke: "primary"
528
+ },
529
+ {
530
+ d: "M18 19l2 -2",
531
+ stroke: "primary"
532
+ }
533
+ ],
534
+ italic: [
535
+ "M11 5l6 0",
536
+ "M7 19l6 0",
537
+ "M14 5l-4 14"
538
+ ],
539
+ line: [{
540
+ d: "M20 4l-16 16",
541
+ stroke: "primary"
542
+ }],
543
+ lineArrow: [{
544
+ d: "M20 4l-16 16",
545
+ stroke: "primary"
546
+ }, {
547
+ d: "M16 3h5v5",
548
+ stroke: "primary"
549
+ }],
550
+ link: [
551
+ "M9 15l6 -6",
552
+ "M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464",
553
+ "M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"
554
+ ],
555
+ linkOff: [
556
+ "M9 15l3 -3m2 -2l1 -1",
557
+ "M11 6l.463 -.536a5 5 0 0 1 7.072 7.071l-.534 .464",
558
+ "M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463",
559
+ "M3 3l18 18"
560
+ ],
561
+ listTree: [
562
+ "M9 6h11",
563
+ "M12 12h8",
564
+ "M15 18h5",
565
+ "M5 6v.01",
566
+ "M8 12v.01",
567
+ "M11 18v.01"
568
+ ],
569
+ lock: ["M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1 -2 2H5a2 2 0 0 1 -2 -2v-7a2 2 0 0 1 2 -2z", "M7 11V7a5 5 0 0 1 10 0v4"],
570
+ marqueeSelect: [
571
+ "M17 20h6",
572
+ "M20 17v6",
573
+ "M4 13v-2",
574
+ "M4 6v-1a1 1 0 0 1 1 -1h1",
575
+ "M6 20h-1a1 1 0 0 1 -1 -1v-1",
576
+ "M11 4h2",
577
+ "M11 20h2",
578
+ "M18 4h1a1 1 0 0 1 1 1v1",
579
+ "M20 11v2"
580
+ ],
581
+ menu: ["M4 8l16 0", "M4 16l16 0"],
582
+ message: [
583
+ "M8 9h8",
584
+ "M8 13h6",
585
+ "M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"
586
+ ],
587
+ palette: [
588
+ "M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25",
589
+ "M8.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
590
+ "M12.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
591
+ "M16.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"
592
+ ],
593
+ paperclip: ["M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"],
594
+ pencilMarker: ["m9.109 16.275 8.856-8.097c.812-.743.87-2.014.127-2.826s-2.014-.869-2.826-.127L6.41 13.322l-.127 2.826zM13.79 6.575l2.7 2.952", {
595
+ d: "M19.375 20.125c.569.063-4.05-.562-6.412-.437s-4.759 1.229-6.857 1.625c-1.764.687-3.404-.938-1.981-2.5",
596
+ stroke: "primary"
597
+ }],
598
+ photo: [
599
+ "M15 8h.01",
600
+ "M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z",
601
+ "M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5",
602
+ "M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"
603
+ ],
604
+ plus: ["M12 5l0 14", "M5 12l14 0"],
605
+ pointer: ["M7.904 17.563a1.2 1.2 0 0 0 2.228 .308l2.09 -3.093l4.907 4.907a1.067 1.067 0 0 0 1.509 0l1.047 -1.047a1.067 1.067 0 0 0 0 -1.509l-4.907 -4.907l3.113 -2.09a1.2 1.2 0 0 0 -.309 -2.228l-13.582 -3.904l3.904 13.563z"],
606
+ polygon: [{
607
+ d: "M13.163 2.168l8.021 5.828c.694 .504 .984 1.397 .719 2.212l-3.064 9.43a1.978 1.978 0 0 1 -1.881 1.367h-9.916a1.978 1.978 0 0 1 -1.881 -1.367l-3.064 -9.43a1.978 1.978 0 0 1 .719 -2.212l8.021 -5.828a1.978 1.978 0 0 1 2.326 0z",
608
+ stroke: "primary",
609
+ fill: "secondary"
610
+ }],
611
+ print: [
612
+ "M17 17h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-14a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h2",
613
+ "M17 9v-4a2 2 0 0 0 -2 -2h-6a2 2 0 0 0 -2 2v4",
614
+ "M7 13m0 2a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2z"
615
+ ],
616
+ redact: [
617
+ "M7 4h10",
618
+ "M12 4v8",
619
+ "M-7 24l12 -12",
620
+ "M-3 24l12 -12",
621
+ "M1 24l12 -12",
622
+ "M5 24l12 -12",
623
+ "M9 24l12 -12",
624
+ "M13 24l12 -12",
625
+ "M17 24l12 -12",
626
+ "M21 24l12 -12",
627
+ "M25 24l12 -12"
628
+ ],
629
+ redactArea: [
630
+ "M6 20h-1a2 2 0 0 1 -2 -2v-1",
631
+ "M3 13v-3",
632
+ "M3 6v-1a2 2 0 0 1 2 -2h1",
633
+ "M10 3h3",
634
+ "M17 3h1a2 2 0 0 1 2 2v1",
635
+ "M12 10h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2v-8a2 2 0 0 1 2 -2z",
636
+ "M10 12l2 -2",
637
+ "M10 16l6 -6",
638
+ "M10 20l10 -10",
639
+ "M12 22l10 -10",
640
+ "M16 22l6 -6",
641
+ "M20 22l2 -2"
642
+ ],
643
+ redactCombined: [
644
+ "M6 20h-1a2 2 0 0 1 -2 -2v-1",
645
+ "M3 13v-3",
646
+ "M20 13v-3",
647
+ "M3 6v-1a2 2 0 0 1 2 -2h1",
648
+ "M10 3h3",
649
+ "M17 3h1a2 2 0 0 1 2 2v1",
650
+ "M8 8h8",
651
+ "M12 8v7",
652
+ "M6 19l12 -12",
653
+ "M6 23l12 -12",
654
+ "M10 23l12 -12",
655
+ "M14 27l12 -12",
656
+ "M14 23l12 -12"
657
+ ],
658
+ redactionSidebar: [
659
+ "M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z",
660
+ "M15 4v16",
661
+ "M7 8h5",
662
+ "M7 11h5",
663
+ "M7 14h4"
664
+ ],
665
+ replaceText: [
666
+ {
667
+ d: "M3 7v-2h12v2",
668
+ stroke: "primary"
669
+ },
670
+ {
671
+ d: "M9 5v14",
672
+ stroke: "primary"
673
+ },
674
+ {
675
+ d: "M7 19h4",
676
+ stroke: "primary"
677
+ },
678
+ {
679
+ d: "M20 14l2 2h-3",
680
+ stroke: "primary"
681
+ },
682
+ {
683
+ d: "M20 18l2 -2",
684
+ stroke: "primary"
685
+ },
686
+ {
687
+ d: "M19 16a3 3 0 1 0 2 5.236",
688
+ stroke: "primary"
689
+ }
690
+ ],
691
+ rotateClockwise: ["M4.05 11a8 8 0 1 1 .5 4m-.5 5v-5h5"],
692
+ rotateCounterClockwise: ["M19.95 11a8 8 0 1 0 -.5 4m.5 5v-5h-5"],
693
+ rubberStamp: ["M21 17.85h-18c0 -4.05 1.421 -4.05 3.79 -4.05c5.21 0 1.21 -4.59 1.21 -6.8a4 4 0 1 1 8 0c0 2.21 -4 6.8 1.21 6.8c2.369 0 3.79 0 3.79 4.05", "M5 21h14"],
694
+ rubberStampPlus: [
695
+ "M21 17.85h-18c0 -4.05 1.421 -4.05 3.79 -4.05c5.21 0 1.21 -4.59 1.21 -6.8a4 4 0 0 1 4.9 -4.1",
696
+ "M16 10c0 1.2 -4 3.8 1.21 3.8c2.369 0 3.79 0 3.79 4.05",
697
+ "M5 21h14",
698
+ "M16 5h6",
699
+ "M19 2v6"
700
+ ],
701
+ save: [
702
+ "M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2",
703
+ "M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
704
+ "M14 4l0 4l-6 0l0 -4"
705
+ ],
706
+ screenshot: [
707
+ "M7 19a2 2 0 0 1 -2 -2",
708
+ "M5 13v-2",
709
+ "M5 7a2 2 0 0 1 2 -2",
710
+ "M11 5h2",
711
+ "M17 5a2 2 0 0 1 2 2",
712
+ "M19 11v2",
713
+ "M19 17v4",
714
+ "M21 19h-4",
715
+ "M13 19h-2"
716
+ ],
717
+ search: ["M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0", "M21 21l-6 -6"],
718
+ settings: ["M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z", "M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"],
719
+ sidebar: ["M4 18v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z", "M10 18v-12a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2z"],
720
+ signature: ["M3 17c3.333 -3.333 5 -6 5 -8c0 -3 -1 -3 -2 -3s-2.032 1.085 -2 3c.034 2.048 1.658 4.877 2.5 6c1.5 2 2.5 2.5 3.5 1l2 -3c.333 2.667 1.333 4 3 4c.53 0 2.639 -2 3 -2c.517 0 1.517 .667 3 2"],
721
+ singlePage: ["M5 3m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"],
722
+ square: [{
723
+ d: "M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z",
724
+ stroke: "primary",
725
+ fill: "secondary"
726
+ }],
727
+ squares: ["M8 10a2 2 0 0 1 2 -2h9a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-9a2 2 0 0 1 -2 -2z", "M16 8v-3a2 2 0 0 0 -2 -2h-9a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h3"],
728
+ squiggly: [
729
+ "M8 16v-8a4 4 0 1 1 8 0v8",
730
+ "M8 10h8",
731
+ {
732
+ d: "M4 20c1.5 -1.5 3.5 -1.5 5 0s3.5 1.5 5 0 3.5 -1.5 5 0",
733
+ stroke: "primary"
734
+ }
735
+ ],
736
+ strikethrough: ["M8 16v-8a4 4 0 1 1 8 0v8", {
737
+ d: "M4 10h16",
738
+ stroke: "primary"
739
+ }],
740
+ text: [
741
+ "M6.5 15.5m-3.5 0a3.5 3.5 0 1 0 7 0a3.5 3.5 0 1 0 -7 0",
742
+ "M14 19v-10.5a3.5 3.5 0 0 1 7 0v10.5",
743
+ "M14 13h7",
744
+ "M10 12v7"
745
+ ],
746
+ trash: [
747
+ "M4 7l16 0",
748
+ "M10 11l0 6",
749
+ "M14 11l0 6",
750
+ "M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12",
751
+ "M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"
752
+ ],
753
+ underline: [
754
+ {
755
+ d: "M4 20h16",
756
+ stroke: "primary"
757
+ },
758
+ "M8 16v-8a4 4 0 1 1 8 0v8",
759
+ "M8 10h8"
760
+ ],
761
+ ungroup: ["M4 5a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z", "M13 14a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1z"],
762
+ unlock: ["M7 11V7a5 5 0 0 1 9.9-1"],
763
+ vertical: [
764
+ "M8 7l4 -4l4 4",
765
+ "M8 17l4 4l4 -4",
766
+ "M12 3l0 18"
767
+ ],
768
+ viewSettings: [
769
+ "M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",
770
+ "M12 10.5v1.5",
771
+ "M12 16v1.5",
772
+ "M15.031 12.25l-1.299 .75",
773
+ "M10.268 15l-1.3 .75",
774
+ "M15 15.803l-1.285 -.773",
775
+ "M10.285 12.97l-1.285 -.773",
776
+ "M14 3v4a1 1 0 0 0 1 1h4",
777
+ "M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"
778
+ ],
779
+ widgetEdit: [
780
+ "M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1",
781
+ "M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415",
782
+ "M16 5l3 3"
783
+ ],
784
+ x: ["M18 6l-12 12", "M6 6l12 12"],
785
+ zigzag: [{
786
+ d: "M12 2.4L21.36 11.76L2.64 12.24L12 21.6",
787
+ stroke: "primary"
788
+ }],
789
+ zoomIn: [
790
+ "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0",
791
+ "M9 12h6",
792
+ "M12 9v6"
793
+ ],
794
+ zoomInArea: [
795
+ "M15 13v4",
796
+ "M13 15h4",
797
+ "M15 15m-5 0a5 5 0 1 0 10 0a5 5 0 1 0 -10 0",
798
+ "M22 22l-3 -3",
799
+ "M6 18h-1a2 2 0 0 1 -2 -2v-1",
800
+ "M3 11v-1",
801
+ "M3 6v-1a2 2 0 0 1 2 -2h1",
802
+ "M10 3h1",
803
+ "M15 3h1a2 2 0 0 1 2 2v1"
804
+ ],
805
+ zoomOut: ["M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0", "M9 12l6 0"]
806
+ };
807
+ /** One SVG renderer for every icon. Unknown names render nothing (caller shows
808
+ * a text fallback), so a missing glyph never throws. */
809
+ function Icon({ name, size = 20, strokeWidth = 2, className, title, accent }) {
810
+ const paths = useCustomIcons()[name] ?? ICON_PATHS[name];
811
+ if (!paths) return null;
812
+ return /* @__PURE__ */ jsx("svg", {
813
+ width: size,
814
+ height: size,
815
+ viewBox: "0 0 24 24",
816
+ fill: "none",
817
+ stroke: "currentColor",
818
+ strokeWidth,
819
+ strokeLinecap: "round",
820
+ strokeLinejoin: "round",
821
+ className,
822
+ role: "img",
823
+ "aria-hidden": title ? void 0 : true,
824
+ "aria-label": title,
825
+ children: paths.map((p, i) => {
826
+ if (typeof p === "string") return /* @__PURE__ */ jsx("path", { d: p }, i);
827
+ const fill = p.fill === true ? "currentColor" : p.fill ? accent?.[p.fill] : void 0;
828
+ const stroke = p.stroke ? accent?.[p.stroke] : p.fill ? "none" : void 0;
829
+ return /* @__PURE__ */ jsx("path", {
830
+ d: p.d,
831
+ fill: fill ?? "none",
832
+ stroke
833
+ }, i);
834
+ })
835
+ });
836
+ }
837
+ //#endregion
838
+ //#region src/ui/theme.tsx
839
+ /**
840
+ * Light/dark theme — one class, and the CSS variables in styles.css do the
841
+ * rest (both toolbars, sidebars, popovers, and the canvas re-theme from it).
842
+ * The mode comes from the embedder's `theme` preference; the chrome ships no
843
+ * switch of its own, so a host that wants one drives this through `set`. The
844
+ * class lands on <html> by default; a delivery that must not touch the page
845
+ * (the <embedpdf-viewer> shadow root) passes `target` and the class stays
846
+ * inside its own subtree. An explicit preference outranks the OS setting.
847
+ */
848
+ const ThemeCtx = createContext(null);
849
+ const systemMode = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
850
+ function ThemeProvider({ children, preference = "system", target }) {
851
+ const [mode, setMode] = useState(() => preference === "system" ? systemMode() : preference);
852
+ useEffect(() => {
853
+ (target ?? document.documentElement).classList.toggle("dark", mode === "dark");
854
+ }, [mode, target]);
855
+ const value = useMemo(() => ({
856
+ mode,
857
+ toggle: () => setMode((m) => m === "dark" ? "light" : "dark"),
858
+ set: setMode
859
+ }), [mode]);
860
+ return /* @__PURE__ */ jsx(ThemeCtx.Provider, {
861
+ value,
862
+ children
863
+ });
864
+ }
865
+ //#endregion
866
+ //#region src/ui/menu.tsx
867
+ /**
868
+ * Menu rendering — a MenuSchema (a tree of command ids) rendered as rows.
869
+ * Reused by the toolbar dropdowns (document / zoom / page-settings) AND by the
870
+ * overflow menu's inline submenu expansion, so a command's menu form is
871
+ * identical wherever it surfaces. Rows resolve live through the command
872
+ * registry: label, icon, active tick, enabled state.
873
+ */
874
+ function MenuRow({ commandId, onRun }) {
875
+ const cmd = useCommand(commandId);
876
+ const commands = useCommands();
877
+ if (!cmd || !cmd.visible) return null;
878
+ return /* @__PURE__ */ jsxs("button", {
879
+ type: "button",
880
+ role: cmd.active ? "menuitemradio" : "menuitem",
881
+ part: cmd.active ? "menu-item menu-item-active" : "menu-item",
882
+ "aria-checked": cmd.active || void 0,
883
+ disabled: !cmd.enabled,
884
+ onClick: () => {
885
+ commands.execute(commandId);
886
+ onRun?.();
887
+ },
888
+ className: `flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors disabled:pointer-events-none disabled:opacity-40 ${cmd.active ? "bg-selected text-accent" : "text-fg-secondary hover:bg-hover"}`,
889
+ children: [cmd.icon && /* @__PURE__ */ jsx(Icon, {
890
+ name: cmd.icon,
891
+ size: 16,
892
+ accent: cmd.iconAccent,
893
+ className: cmd.active ? "" : "text-fg-muted"
894
+ }), /* @__PURE__ */ jsx("span", {
895
+ className: "flex-1 truncate",
896
+ children: cmd.label
897
+ })]
898
+ });
899
+ }
900
+ /** One section-separated menu, by schema id. */
901
+ function MenuBody({ menuId, onRun }) {
902
+ const t = useT();
903
+ const menu = useMenuSchema(menuId);
904
+ if (!menu) return null;
905
+ return /* @__PURE__ */ jsx("div", {
906
+ className: "min-w-52 p-1",
907
+ children: menu.sections.map((section, i) => /* @__PURE__ */ jsxs("div", { children: [
908
+ i > 0 && /* @__PURE__ */ jsx("div", { className: "bg-border-subtle my-1 h-px" }),
909
+ section.labelKey && /* @__PURE__ */ jsx("div", {
910
+ className: "text-fg-muted px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide",
911
+ children: t(section.labelKey)
912
+ }),
913
+ section.items.map((id) => /* @__PURE__ */ jsx(MenuRow, {
914
+ commandId: id,
915
+ onRun
916
+ }, id))
917
+ ] }, i))
918
+ });
919
+ }
920
+ /** The popover shell: a floating surface + click-away scrim. */
921
+ function Popover({ children, onClose, align = "start" }) {
922
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
923
+ className: "fixed inset-0 z-40",
924
+ onClick: onClose
925
+ }), /* @__PURE__ */ jsx("div", {
926
+ part: "menu",
927
+ className: `border-border-subtle bg-elevated absolute top-full z-50 mt-1 rounded-lg border shadow-xl ${align === "end" ? "right-0" : "left-0"}`,
928
+ children
929
+ })] });
930
+ }
931
+ /** A menu dropdown that expands inline (used inside the overflow menu, where a
932
+ * submenu can't anchor to an off-screen trigger). */
933
+ function InlineSubmenu({ menuId, label }) {
934
+ const [open, setOpen] = useState(false);
935
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
936
+ type: "button",
937
+ onClick: () => setOpen((o) => !o),
938
+ className: "text-fg-secondary hover:bg-hover flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-left text-sm",
939
+ children: [/* @__PURE__ */ jsx("span", {
940
+ className: "flex-1 truncate",
941
+ children: label
942
+ }), /* @__PURE__ */ jsx(Icon, {
943
+ name: open ? "chevronDown" : "chevronRight",
944
+ size: 16
945
+ })]
946
+ }), open && /* @__PURE__ */ jsx("div", {
947
+ className: "border-border-subtle ml-3 border-l pl-1",
948
+ children: /* @__PURE__ */ jsx(MenuBody, { menuId })
949
+ })] });
950
+ }
951
+ //#endregion
952
+ //#region src/ui/toolbar.tsx
953
+ /**
954
+ * The app's toolbar look — Tailwind render props over the headless <Toolbar>.
955
+ * The app owns the pixels; ui-core owns the physics (measure → solve →
956
+ * overflow). Nothing here knows about breakpoints or widths.
957
+ *
958
+ * Styling is a 1:1 port of the v2 snippet's components:
959
+ * Button → viewers/snippet/src/components/ui/button.tsx
960
+ * TabButton → viewers/snippet/src/components/ui/tab-button.tsx (underline)
961
+ * zoom widget → viewers/snippet/src/components/custom-zoom-toolbar.tsx
962
+ * mode select → viewers/snippet/src/components/mode-select-button.tsx
963
+ */
964
+ const BTN_BASE = "flex h-8 w-auto min-w-8 cursor-pointer items-center justify-center rounded-md p-[5px] transition-colors";
965
+ const BTN_HOVER = "hover:bg-hover hover:ring hover:ring-accent";
966
+ const BTN_ACTIVE = "border-none bg-selected text-accent shadow ring ring-accent";
967
+ const BTN_DISABLED = "cursor-not-allowed opacity-50 hover:bg-transparent hover:ring-0";
968
+ const buttonClass = (active, enabled = true) => `${BTN_BASE} ${active ? BTN_ACTIVE : BTN_HOVER} ${enabled ? "" : BTN_DISABLED}`;
969
+ const TAB_BASE = "flex h-8 w-auto min-w-8 cursor-pointer items-center justify-center rounded-none border-b-2 px-2 py-1 text-sm transition-colors hover:bg-transparent hover:border-b-fg-muted";
970
+ const TAB_ACTIVE = "border-b-accent text-accent hover:border-b-accent";
971
+ const TAB_INACTIVE = "border-b-transparent";
972
+ const tabClass = (active, enabled = true) => `${TAB_BASE} ${active ? TAB_ACTIVE : TAB_INACTIVE} ${enabled ? "" : "cursor-not-allowed opacity-50"}`;
973
+ function CommandButton({ cmd, variant, run }) {
974
+ const isTab = variant === "label";
975
+ const button = /* @__PURE__ */ jsx("button", {
976
+ type: "button",
977
+ onClick: run,
978
+ disabled: !cmd.enabled,
979
+ "aria-pressed": cmd.active || void 0,
980
+ "aria-haspopup": cmd.menu ? "menu" : void 0,
981
+ "aria-label": cmd.label,
982
+ title: cmd.label,
983
+ part: isTab ? `mode-tab${cmd.active ? " mode-tab-active" : ""}` : `toolbar-button${cmd.active ? " toolbar-button-active" : ""}`,
984
+ className: isTab ? tabClass(cmd.active, cmd.enabled) : buttonClass(cmd.active, cmd.enabled),
985
+ children: isTab ? /* @__PURE__ */ jsx("span", {
986
+ className: "whitespace-nowrap px-1",
987
+ children: cmd.label
988
+ }) : variant === "icon+label" ? /* @__PURE__ */ jsxs("span", {
989
+ className: "flex items-center whitespace-nowrap text-sm",
990
+ children: [cmd.icon && /* @__PURE__ */ jsx(Icon, {
991
+ name: cmd.icon,
992
+ size: 20,
993
+ accent: cmd.iconAccent,
994
+ className: "mr-1.5 shrink-0"
995
+ }), /* @__PURE__ */ jsx("span", { children: cmd.label })]
996
+ }) : cmd.icon ? /* @__PURE__ */ jsx(Icon, {
997
+ name: cmd.icon,
998
+ size: 20,
999
+ accent: cmd.iconAccent
1000
+ }) : /* @__PURE__ */ jsx("span", {
1001
+ className: "text-sm",
1002
+ children: cmd.label
1003
+ })
1004
+ });
1005
+ if (!cmd.menu) return button;
1006
+ return /* @__PURE__ */ jsx(MenuAnchoredButton, {
1007
+ cmd,
1008
+ children: button
1009
+ });
1010
+ }
1011
+ function MenuAnchoredButton({ cmd, children }) {
1012
+ const menus = useMenus();
1013
+ return /* @__PURE__ */ jsxs("span", {
1014
+ className: "relative inline-flex",
1015
+ children: [children, (cmd.menu ? menus.isOpen(cmd.menu) : false) && cmd.menu && /* @__PURE__ */ jsx(Popover, {
1016
+ onClose: () => menus.close(cmd.menu),
1017
+ children: /* @__PURE__ */ jsx(MenuBody, {
1018
+ menuId: cmd.menu,
1019
+ onRun: () => menus.close(cmd.menu)
1020
+ })
1021
+ })]
1022
+ });
1023
+ }
1024
+ function ZoomControls() {
1025
+ const commands = useCommands();
1026
+ const menus = useMenus();
1027
+ const stage = useOptionalCapability(StageToken$1);
1028
+ const level = useOptionalSelector(StageToken$1, (c) => c.zoomLevel(), 1);
1029
+ const pct = Math.round((level ?? 1) * 100);
1030
+ const [inputValue, setInputValue] = useState(String(pct));
1031
+ useEffect(() => setInputValue(String(pct)), [pct]);
1032
+ const submit = (e) => {
1033
+ e.preventDefault();
1034
+ const value = parseFloat(inputValue);
1035
+ if (!isNaN(value) && value > 0) stage?.zoomTo({ level: value / 100 });
1036
+ };
1037
+ return /* @__PURE__ */ jsxs("div", {
1038
+ className: "relative",
1039
+ children: [/* @__PURE__ */ jsxs("div", {
1040
+ className: "bg-hover flex items-center rounded",
1041
+ children: [
1042
+ /* @__PURE__ */ jsxs("form", {
1043
+ onSubmit: submit,
1044
+ className: "flex min-w-0 flex-nowrap items-center overflow-hidden whitespace-nowrap",
1045
+ children: [/* @__PURE__ */ jsx("input", {
1046
+ name: "zoom",
1047
+ type: "text",
1048
+ inputMode: "numeric",
1049
+ pattern: "\\d*",
1050
+ "aria-label": "Set zoom",
1051
+ className: "h-6 w-8 min-w-0 shrink border-0 bg-transparent p-0 text-right text-sm outline-none focus:outline-none",
1052
+ value: inputValue,
1053
+ onChange: (e) => setInputValue(e.target.value.replace(/[^0-9]/g, "")),
1054
+ onBlur: () => {
1055
+ if (!inputValue || parseFloat(inputValue) <= 0) setInputValue(String(pct));
1056
+ }
1057
+ }), /* @__PURE__ */ jsx("span", {
1058
+ className: "shrink-0 text-sm",
1059
+ children: "%"
1060
+ })]
1061
+ }),
1062
+ /* @__PURE__ */ jsx("button", {
1063
+ type: "button",
1064
+ "aria-haspopup": "menu",
1065
+ "aria-expanded": menus.isOpen("zoom"),
1066
+ title: "Zoom options",
1067
+ className: buttonClass(false),
1068
+ onClick: () => menus.toggle("zoom"),
1069
+ children: /* @__PURE__ */ jsx(Icon, {
1070
+ name: "chevronDown",
1071
+ size: 20
1072
+ })
1073
+ }),
1074
+ /* @__PURE__ */ jsx("button", {
1075
+ type: "button",
1076
+ title: "Zoom out",
1077
+ className: buttonClass(false),
1078
+ onClick: () => commands.execute("zoom:out"),
1079
+ children: /* @__PURE__ */ jsx(Icon, {
1080
+ name: "zoomOut",
1081
+ size: 20
1082
+ })
1083
+ }),
1084
+ /* @__PURE__ */ jsx("button", {
1085
+ type: "button",
1086
+ title: "Zoom in",
1087
+ className: buttonClass(false),
1088
+ onClick: () => commands.execute("zoom:in"),
1089
+ children: /* @__PURE__ */ jsx(Icon, {
1090
+ name: "zoomIn",
1091
+ size: 20
1092
+ })
1093
+ })
1094
+ ]
1095
+ }), menus.isOpen("zoom") && /* @__PURE__ */ jsx(Popover, {
1096
+ onClose: () => menus.close("zoom"),
1097
+ children: /* @__PURE__ */ jsx(MenuBody, {
1098
+ menuId: "zoom",
1099
+ onRun: () => menus.close("zoom")
1100
+ })
1101
+ })]
1102
+ });
1103
+ }
1104
+ /** The 'button' variant of the zoom slot — a single icon that opens the menu. */
1105
+ function ZoomButton() {
1106
+ const menus = useMenus();
1107
+ return /* @__PURE__ */ jsxs("span", {
1108
+ className: "relative inline-flex",
1109
+ children: [/* @__PURE__ */ jsx("button", {
1110
+ type: "button",
1111
+ title: "Zoom",
1112
+ "aria-haspopup": "menu",
1113
+ "aria-expanded": menus.isOpen("zoom"),
1114
+ className: buttonClass(menus.isOpen("zoom")),
1115
+ onClick: () => menus.toggle("zoom"),
1116
+ children: /* @__PURE__ */ jsx(Icon, {
1117
+ name: "zoomIn",
1118
+ size: 20
1119
+ })
1120
+ }), menus.isOpen("zoom") && /* @__PURE__ */ jsx(Popover, {
1121
+ onClose: () => menus.close("zoom"),
1122
+ children: /* @__PURE__ */ jsx(MenuBody, {
1123
+ menuId: "zoom",
1124
+ onRun: () => menus.close("zoom")
1125
+ })
1126
+ })]
1127
+ });
1128
+ }
1129
+ function GroupTrigger({ view }) {
1130
+ const [open, setOpen] = useState(false);
1131
+ const activeHidden = view.commands.some((c) => c.active);
1132
+ return /* @__PURE__ */ jsxs("span", {
1133
+ className: "relative inline-flex",
1134
+ children: [/* @__PURE__ */ jsx("button", {
1135
+ type: "button",
1136
+ onClick: () => setOpen((o) => !o),
1137
+ "aria-haspopup": "menu",
1138
+ "aria-expanded": open,
1139
+ className: tabClass(activeHidden),
1140
+ children: /* @__PURE__ */ jsx(Icon, {
1141
+ name: "chevronDown",
1142
+ size: 18
1143
+ })
1144
+ }), open && /* @__PURE__ */ jsx(Popover, {
1145
+ onClose: () => setOpen(false),
1146
+ children: /* @__PURE__ */ jsx("div", {
1147
+ className: "min-w-44 p-1",
1148
+ children: view.commands.map((c) => /* @__PURE__ */ jsx("button", {
1149
+ type: "button",
1150
+ role: view.role === "tabs" ? "menuitemradio" : "menuitem",
1151
+ "aria-checked": view.role === "tabs" ? c.active : void 0,
1152
+ disabled: !c.enabled,
1153
+ onClick: () => {
1154
+ view.execute(c.id);
1155
+ setOpen(false);
1156
+ },
1157
+ className: `flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm disabled:cursor-not-allowed disabled:opacity-50 ${c.active ? "bg-selected text-accent" : "text-fg-secondary hover:bg-hover"}`,
1158
+ children: c.label
1159
+ }, c.id))
1160
+ })
1161
+ })]
1162
+ });
1163
+ }
1164
+ function CollapsedModes({ view }) {
1165
+ const [open, setOpen] = useState(false);
1166
+ const active = view.commands.find((c) => c.active) ?? view.commands[0];
1167
+ const isActive = Boolean(active?.active && active.id !== "mode:view");
1168
+ return /* @__PURE__ */ jsxs("div", {
1169
+ style: { width: 100 },
1170
+ className: "relative",
1171
+ children: [/* @__PURE__ */ jsxs("button", {
1172
+ type: "button",
1173
+ onClick: () => setOpen((o) => !o),
1174
+ "aria-haspopup": "menu",
1175
+ "aria-expanded": open,
1176
+ className: "bg-surface hover:bg-hover flex w-full cursor-pointer appearance-none items-center rounded-md py-1.5 pl-3 pr-2 text-[13px] transition-colors",
1177
+ children: [/* @__PURE__ */ jsx("span", {
1178
+ className: `min-w-0 flex-1 truncate text-left ${isActive ? "text-accent" : "text-fg"}`,
1179
+ children: active?.label
1180
+ }), /* @__PURE__ */ jsx(Icon, {
1181
+ name: "chevronDown",
1182
+ size: 16,
1183
+ className: isActive ? "text-accent" : "text-fg-secondary"
1184
+ })]
1185
+ }), open && /* @__PURE__ */ jsx(Popover, {
1186
+ onClose: () => setOpen(false),
1187
+ children: /* @__PURE__ */ jsx("div", {
1188
+ className: "min-w-44 p-1",
1189
+ children: view.commands.map((c) => /* @__PURE__ */ jsx("button", {
1190
+ type: "button",
1191
+ role: "menuitemradio",
1192
+ "aria-checked": c.active,
1193
+ onClick: () => {
1194
+ view.execute(c.id);
1195
+ setOpen(false);
1196
+ },
1197
+ className: `flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm ${c.active ? "bg-selected text-accent" : "text-fg-secondary hover:bg-hover"}`,
1198
+ children: c.label
1199
+ }, c.id))
1200
+ })
1201
+ })]
1202
+ });
1203
+ }
1204
+ function OverflowMenu({ view }) {
1205
+ const t = useT();
1206
+ if (!view.isOpen) return null;
1207
+ return /* @__PURE__ */ jsx(Popover, {
1208
+ onClose: view.close,
1209
+ align: "end",
1210
+ children: /* @__PURE__ */ jsx("div", {
1211
+ className: "min-w-56 p-1",
1212
+ children: view.sections.map((section, i) => /* @__PURE__ */ jsxs("div", { children: [
1213
+ i > 0 && /* @__PURE__ */ jsx("div", { className: "bg-border-subtle my-1 h-px" }),
1214
+ section.labelKey && /* @__PURE__ */ jsx("div", {
1215
+ className: "text-fg-muted px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide",
1216
+ children: t(section.labelKey)
1217
+ }),
1218
+ section.rows.map((row) => {
1219
+ const cmd = view.resolve(row.command);
1220
+ if (!cmd) return null;
1221
+ if (row.type === "submenu") return /* @__PURE__ */ jsx(InlineSubmenu, {
1222
+ menuId: row.menu,
1223
+ label: cmd.label
1224
+ }, row.command);
1225
+ return /* @__PURE__ */ jsxs("button", {
1226
+ type: "button",
1227
+ role: section.role === "radio" ? "menuitemradio" : "menuitem",
1228
+ "aria-checked": section.role === "radio" ? cmd.active : void 0,
1229
+ disabled: !cmd.enabled,
1230
+ onClick: () => {
1231
+ view.execute(row.command);
1232
+ view.close();
1233
+ },
1234
+ className: `flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${cmd.active ? "bg-selected text-accent" : "text-fg-secondary hover:bg-hover"}`,
1235
+ children: [cmd.icon && /* @__PURE__ */ jsx(Icon, {
1236
+ name: cmd.icon,
1237
+ size: 16,
1238
+ className: cmd.active ? "" : "text-fg-muted"
1239
+ }), /* @__PURE__ */ jsx("span", {
1240
+ className: "flex-1 truncate",
1241
+ children: cmd.label
1242
+ })]
1243
+ }, row.command);
1244
+ })
1245
+ ] }, i))
1246
+ })
1247
+ });
1248
+ }
1249
+ function OverflowTrigger({ isOpen, toggle }) {
1250
+ return /* @__PURE__ */ jsx("button", {
1251
+ type: "button",
1252
+ onClick: toggle,
1253
+ "aria-haspopup": "menu",
1254
+ "aria-expanded": isOpen,
1255
+ title: useT()("demo.more"),
1256
+ className: buttonClass(isOpen),
1257
+ children: /* @__PURE__ */ jsx(Icon, {
1258
+ name: "dots",
1259
+ size: 20
1260
+ })
1261
+ });
1262
+ }
1263
+ function AppToolbar({ bar, className }) {
1264
+ return /* @__PURE__ */ jsx(Toolbar, {
1265
+ bar,
1266
+ gap: 8,
1267
+ separatorWidth: 1,
1268
+ className,
1269
+ renderCommand: (cmd, variant, run) => /* @__PURE__ */ jsx(CommandButton, {
1270
+ cmd,
1271
+ variant,
1272
+ run
1273
+ }),
1274
+ renderCustom: { "zoom-controls": (variant) => variant === "inline" ? /* @__PURE__ */ jsx(ZoomControls, {}) : /* @__PURE__ */ jsx(ZoomButton, {}) },
1275
+ renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ jsx(CollapsedModes, { view }) : void 0,
1276
+ renderGroupTrigger: (view) => /* @__PURE__ */ jsx(GroupTrigger, { view }),
1277
+ renderSeparator: () => /* @__PURE__ */ jsx("span", { className: "bg-border h-6 w-px self-center" }),
1278
+ renderOverflowTrigger: (isOpen, toggle) => /* @__PURE__ */ jsx(OverflowTrigger, {
1279
+ isOpen,
1280
+ toggle
1281
+ }),
1282
+ renderOverflowMenu: (view) => /* @__PURE__ */ jsx(OverflowMenu, { view })
1283
+ });
1284
+ }
1285
+ //#endregion
1286
+ //#region src/ui/annotation-strip.tsx
1287
+ /**
1288
+ * The annotation selection strip — v2's floating "selection menu" rebuilt as
1289
+ * chrome. Three parties, none of them this file's business to compute:
1290
+ *
1291
+ * config/chrome.ts `strips.annotation` declares WHAT may appear
1292
+ * each command's visible/enabled decides WHICH items show
1293
+ * <AnnotationMenu> solves WHERE (camera transform,
1294
+ * rotate-knob dodging, pointer isolation)
1295
+ *
1296
+ * useStripView is the live intersection: hidden commands drop out, empty
1297
+ * groups vanish, null when nothing applies. This file only draws pixels.
1298
+ * Mounted in the Stage `overlay` slot.
1299
+ */
1300
+ function StripButton({ cmd, run }) {
1301
+ return /* @__PURE__ */ jsx("button", {
1302
+ type: "button",
1303
+ onClick: run,
1304
+ disabled: !cmd.enabled,
1305
+ "aria-pressed": cmd.active || void 0,
1306
+ "aria-label": cmd.label,
1307
+ title: cmd.label,
1308
+ className: buttonClass(cmd.active, cmd.enabled),
1309
+ children: cmd.icon && /* @__PURE__ */ jsx(Icon, {
1310
+ name: cmd.icon,
1311
+ size: 20,
1312
+ accent: cmd.iconAccent
1313
+ })
1314
+ });
1315
+ }
1316
+ function AnnotationStrip() {
1317
+ const t = useT();
1318
+ const view = useStripView(useStripSchema("annotation"));
1319
+ if (!view) return null;
1320
+ return /* @__PURE__ */ jsx(AnnotationMenu, {
1321
+ placement: "bottom",
1322
+ gap: 15,
1323
+ children: () => /* @__PURE__ */ jsx("div", {
1324
+ role: "toolbar",
1325
+ "aria-label": t("commands.annotate.strip"),
1326
+ className: "border-border-subtle bg-elevated flex items-center gap-1 rounded-lg border p-1 shadow-xl",
1327
+ children: view.groups.map((g, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && /* @__PURE__ */ jsx("span", {
1328
+ "aria-hidden": true,
1329
+ className: "bg-border h-5 w-px"
1330
+ }), g.commands.map((cmd) => /* @__PURE__ */ jsx(StripButton, {
1331
+ cmd,
1332
+ run: () => view.execute(cmd.id)
1333
+ }, cmd.id))] }, g.id))
1334
+ })
1335
+ });
1336
+ }
1337
+ //#endregion
1338
+ //#region src/ui/tab-bar.tsx
1339
+ /**
1340
+ * The document tab bar — a 1:1 port of v2's tab-bar.tsx
1341
+ * (viewers/snippet/src/components/tab-bar.tsx): rounded-top tabs on the
1342
+ * surface-alt strip, the active tab merging into the toolbar surface below,
1343
+ * close-× on the active tab, and a + button opening a real file dialog.
1344
+ *
1345
+ * v2 needed a DocumentManager capability; here the kernel's document registry
1346
+ * (useDocuments) already is the tab model — open/close/setActive, reactive.
1347
+ */
1348
+ function TabBar({ visibility = "always", allowOpenFile = true }) {
1349
+ const { docs, activeId, setActive, close, open } = useDocuments();
1350
+ const fileInput = useRef(null);
1351
+ if (!(visibility === "always" || visibility === "multiple" && docs.length > 1)) return null;
1352
+ const openFile = async (file) => {
1353
+ const bytes = new Uint8Array(await file.arrayBuffer());
1354
+ const source = {
1355
+ kind: "bytes",
1356
+ id: `${file.name}-${Date.now()}`,
1357
+ bytes
1358
+ };
1359
+ await open(source, { name: file.name });
1360
+ };
1361
+ return /* @__PURE__ */ jsx("div", {
1362
+ className: "bg-surface-alt flex items-end pr-2 pt-2",
1363
+ children: /* @__PURE__ */ jsxs("div", {
1364
+ className: "flex flex-1 items-end overflow-x-auto pl-4",
1365
+ children: [docs.map((doc) => {
1366
+ const isActive = activeId === doc.id;
1367
+ return /* @__PURE__ */ jsxs("div", {
1368
+ onClick: () => setActive(doc.id),
1369
+ role: "tab",
1370
+ part: isActive ? "tab tab-active" : "tab",
1371
+ tabIndex: 0,
1372
+ "aria-selected": isActive,
1373
+ onKeyDown: (e) => {
1374
+ if (e.key === "Enter" || e.key === " ") {
1375
+ e.preventDefault();
1376
+ setActive(doc.id);
1377
+ }
1378
+ },
1379
+ className: `group relative flex min-w-[120px] max-w-[240px] cursor-pointer items-center gap-2 rounded-t-lg px-4 py-2 text-sm font-medium transition-all ${isActive ? "bg-surface text-fg z-10" : "bg-surface-alt text-fg-secondary hover:bg-hover hover:text-fg"}`,
1380
+ children: [
1381
+ doc.status === "loading" && /* @__PURE__ */ jsx("span", {
1382
+ className: "border-border-subtle border-t-accent h-3 w-3 flex-shrink-0 animate-spin rounded-full border",
1383
+ "aria-label": "Loading"
1384
+ }),
1385
+ doc.status === "locked" && /* @__PURE__ */ jsx(Icon, {
1386
+ name: "lock",
1387
+ size: 13,
1388
+ className: "text-fg-muted flex-shrink-0"
1389
+ }),
1390
+ doc.status === "error" && /* @__PURE__ */ jsx(Icon, {
1391
+ name: "alertTriangle",
1392
+ size: 13,
1393
+ className: "flex-shrink-0 text-red-500"
1394
+ }),
1395
+ /* @__PURE__ */ jsx("span", {
1396
+ className: "min-w-0 flex-1 truncate",
1397
+ children: doc.name ?? `Document ${doc.id.slice(0, 8)}`
1398
+ }),
1399
+ isActive && docs.length > 1 && /* @__PURE__ */ jsx("button", {
1400
+ type: "button",
1401
+ onClick: (e) => {
1402
+ e.stopPropagation();
1403
+ close(doc.id);
1404
+ },
1405
+ "aria-label": `Close ${doc.name ?? "document"}`,
1406
+ className: "hover:bg-hover flex-shrink-0 cursor-pointer rounded-full p-1 opacity-100 transition-all",
1407
+ children: /* @__PURE__ */ jsx(Icon, {
1408
+ name: "x",
1409
+ size: 14
1410
+ })
1411
+ })
1412
+ ]
1413
+ }, doc.id);
1414
+ }), allowOpenFile && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
1415
+ type: "button",
1416
+ onClick: () => fileInput.current?.click(),
1417
+ "aria-label": "Open File",
1418
+ title: "Open File",
1419
+ className: "text-fg-secondary hover:bg-hover hover:text-fg mb-1.5 ml-1 flex-shrink-0 cursor-pointer rounded p-1.5 transition-colors",
1420
+ children: /* @__PURE__ */ jsx(Icon, {
1421
+ name: "plus",
1422
+ size: 14
1423
+ })
1424
+ }), /* @__PURE__ */ jsx("input", {
1425
+ ref: fileInput,
1426
+ type: "file",
1427
+ accept: ".pdf,application/pdf",
1428
+ className: "hidden",
1429
+ onChange: (e) => {
1430
+ const file = e.target.files?.[0];
1431
+ if (file) openFile(file);
1432
+ e.target.value = "";
1433
+ }
1434
+ })] })]
1435
+ })
1436
+ });
1437
+ }
1438
+ //#endregion
1439
+ //#region src/ui/tool-cursor.tsx
1440
+ /**
1441
+ * THIS viewer's armed-tool cursor: the toolbar's own icon riding the pointer
1442
+ * as a real CSS cursor (OS-composited — zero lag, unlike any DOM follower).
1443
+ * The library owns arbitration (unmapped hover claims win, page gaps fall
1444
+ * back to the arrow — see `useToolCursor`); this module only decides what the
1445
+ * cursor LOOKS like. It reuses the SAME icon set and the SAME accent
1446
+ * derivation as the toolbar buttons (TOOL_ICONS is recorded by the command
1447
+ * definitions), so the cursor is pixel-identical to the button the user just
1448
+ * pressed — recolor the tool and both follow. A tool without a command icon
1449
+ * keeps its declared keyword cursor.
1450
+ *
1451
+ * ONE table decides the shape ({@link GLYPHS}): cursor KEYWORDS are the
1452
+ * affordances — a tool declares its base ('crosshair' to draw, 'copy' to
1453
+ * place), the selection handler claims 'text' over text — and each mapped
1454
+ * keyword is redrawn as its glyph + the tool's icon. Keywords the table omits
1455
+ * stay native: a markup tool's 'default' base is the bare arrow (identity
1456
+ * appears exactly where the action is possible — over text), a foreign 'move'
1457
+ * over an annotation drops the icon. A new tool gets the right cursor from
1458
+ * what it declares; nothing tool-specific lives here.
1459
+ *
1460
+ * The image: a white-haloed glyph at the hotspot (readable on any page color)
1461
+ * with the icon at its top right — 40px, under the 128px cursor ceiling;
1462
+ * Chromium may briefly show the default arrow when a >32px cursor would
1463
+ * overlap browser UI (anti-spoofing), which is fine mid-viewport.
1464
+ */
1465
+ const SIZE = 40;
1466
+ const HOTSPOT = {
1467
+ x: 8,
1468
+ y: 28
1469
+ };
1470
+ const INK = "#1a1a1a";
1471
+ const HALO = "#ffffff";
1472
+ /** The drag-and-drop "will place a copy" green (Chrome's DnD badge). */
1473
+ const PLACE_GREEN = "#34a853";
1474
+ /** The icon's paths as SVG markup — the string twin of `<Icon>`'s render loop
1475
+ * (same fill/stroke slot rules), with a white halo pass underneath so the
1476
+ * glyph stays readable over page content. */
1477
+ function iconGlyph(name, accent) {
1478
+ const paths = ICON_PATHS[name];
1479
+ if (!paths) return null;
1480
+ const halo = [];
1481
+ const draw = [];
1482
+ for (const p of paths) {
1483
+ const spec = typeof p === "string" ? {
1484
+ d: p,
1485
+ fill: void 0,
1486
+ stroke: void 0
1487
+ } : p;
1488
+ const fill = spec.fill === true ? INK : spec.fill ? accent?.[spec.fill] : void 0;
1489
+ const stroke = spec.stroke ? accent?.[spec.stroke] ?? INK : spec.fill ? "none" : INK;
1490
+ halo.push(`<path d="${spec.d}" fill="none" stroke="${HALO}" stroke-width="3.5"/>`);
1491
+ draw.push(`<path d="${spec.d}" fill="${fill ?? "none"}" stroke="${stroke}"/>`);
1492
+ }
1493
+ return halo.join("") + draw.join("");
1494
+ }
1495
+ const haloed = (x1, y1, x2, y2) => `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${HALO}" stroke-width="3"/><line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${INK}" stroke-width="1.25"/>`;
1496
+ function actionGlyph(glyph) {
1497
+ const { x, y } = HOTSPOT;
1498
+ if (glyph === "crosshair") return haloed(x, y - 6, x, y + 6) + haloed(x - 6, y, x + 6, y);
1499
+ if (glyph === "ibeam") return haloed(x, y - 6.5, x, y + 6.5) + haloed(x - 2.5, y - 6.5, x + 2.5, y - 6.5) + haloed(x - 2.5, y + 6.5, x + 2.5, y + 6.5);
1500
+ return `<circle cx="${x}" cy="${y}" r="7.5" fill="${PLACE_GREEN}" stroke="${HALO}" stroke-width="1.5"/><line x1="${x}" y1="${y - 3.5}" x2="${x}" y2="${y + 3.5}" stroke="${HALO}" stroke-width="2"/><line x1="${x - 3.5}" y1="${y}" x2="${x + 3.5}" y2="${y}" stroke="${HALO}" stroke-width="2"/>`;
1501
+ }
1502
+ function cursorSvg(glyph, icon, accent) {
1503
+ const g = iconGlyph(icon, accent);
1504
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}" fill="none" stroke-linecap="round" stroke-linejoin="round">` + actionGlyph(glyph) + (g ? `<g transform="translate(17 3) scale(${(20 / 24).toFixed(4)})" stroke-width="2">${g}</g>` : "") + `</svg>`;
1505
+ }
1506
+ /** keyword → glyph: how each affordance is drawn when a tool icon rides
1507
+ * along. Draw tools declare 'crosshair', place tools (stamp/signature/image)
1508
+ * declare 'copy', and the selection handler claims 'text' over text — which
1509
+ * keywords ever SHOW is decided by those declarations and claims, never
1510
+ * here. 'default' is deliberately absent: the bare arrow means "no action
1511
+ * here", so it never carries an icon. */
1512
+ const GLYPHS = {
1513
+ crosshair: "crosshair",
1514
+ text: "ibeam",
1515
+ copy: "plus"
1516
+ };
1517
+ /** Mount once (renders nothing): keeps the armed tool's cursor skin in sync
1518
+ * with its toolbar icon + live defaults. */
1519
+ function ArmedToolCursor() {
1520
+ const { activeToolId } = useTool();
1521
+ const d = useAnnotationDefaults(activeToolId);
1522
+ const entry = TOOL_ICONS[activeToolId];
1523
+ const accent = entry?.accent ? {
1524
+ primary: d[entry.accent.primary] ?? void 0,
1525
+ secondary: entry.accent.secondary ? d[entry.accent.secondary] ?? void 0 : void 0
1526
+ } : void 0;
1527
+ useToolCursor(entry ? {
1528
+ toolId: activeToolId,
1529
+ cursors: Object.fromEntries(Object.entries(GLYPHS).map(([keyword, glyph]) => [keyword, {
1530
+ svg: cursorSvg(glyph, entry.icon, accent),
1531
+ hotspot: HOTSPOT
1532
+ }]))
1533
+ } : null);
1534
+ return null;
1535
+ }
1536
+ //#endregion
1537
+ //#region src/ui/annotation-flags.tsx
1538
+ /**
1539
+ * The annotation FLAGS section — a live test surface for `/F` (ISO 32000
1540
+ * Table 167). Select any annotation(s) and toggle flags; the writes go through
1541
+ * the plugin's `updateSelectionFlags` (optimistic, flags-only engine patch),
1542
+ * and the reads come from `useSelectionFlags` (per-flag value, `null` when the
1543
+ * selection disagrees → indeterminate checkbox).
1544
+ *
1545
+ * Things to try:
1546
+ * - `locked`: the selection keeps its outline but loses handles/knob; move,
1547
+ * restyle and delete are refused; unchecking it here still works.
1548
+ * - `hidden` / `noView`: the annotation disappears (it stays selected, so
1549
+ * this panel can bring it back).
1550
+ * - `readOnly`: visible but no longer clickable — deselect and try.
1551
+ * - `noZoom` / `noRotate`: zoom or rotate the page; the body keeps its
1552
+ * screen size / stays upright, anchored at its top-left corner.
1553
+ * - `print`: flip it off and print/flatten elsewhere — the annotation is
1554
+ * excluded (viewer rendering is unaffected).
1555
+ */
1556
+ const FLAG_ROWS = [
1557
+ {
1558
+ key: "print",
1559
+ label: "Print",
1560
+ hint: "Include when the page is printed"
1561
+ },
1562
+ {
1563
+ key: "hidden",
1564
+ label: "Hidden",
1565
+ hint: "Gone everywhere: screen, clicks, print"
1566
+ },
1567
+ {
1568
+ key: "noView",
1569
+ label: "No view",
1570
+ hint: "Hidden on screen, may still print"
1571
+ },
1572
+ {
1573
+ key: "toggleNoView",
1574
+ label: "Toggle no view",
1575
+ hint: "Reveal a no-view annotation while selected"
1576
+ },
1577
+ {
1578
+ key: "readOnly",
1579
+ label: "Read only",
1580
+ hint: "Visible but not clickable"
1581
+ },
1582
+ {
1583
+ key: "locked",
1584
+ label: "Locked",
1585
+ hint: "No move / resize / delete / restyle"
1586
+ },
1587
+ {
1588
+ key: "lockedContents",
1589
+ label: "Locked contents",
1590
+ hint: "Text cannot change; geometry can"
1591
+ },
1592
+ {
1593
+ key: "noZoom",
1594
+ label: "No zoom",
1595
+ hint: "Screen-constant size (anchored top-left)"
1596
+ },
1597
+ {
1598
+ key: "noRotate",
1599
+ label: "No rotate",
1600
+ hint: "Stays upright when the page rotates"
1601
+ },
1602
+ {
1603
+ key: "invisible",
1604
+ label: "Invisible",
1605
+ hint: "Legacy: hide unknown subtypes"
1606
+ }
1607
+ ];
1608
+ function FlagRow({ label, hint, value, onToggle }) {
1609
+ const ref = useRef(null);
1610
+ useEffect(() => {
1611
+ if (ref.current) ref.current.indeterminate = value === null;
1612
+ }, [value]);
1613
+ return /* @__PURE__ */ jsxs("label", {
1614
+ className: "hover:bg-hover flex cursor-pointer items-start gap-2.5 rounded px-1.5 py-1.5",
1615
+ children: [/* @__PURE__ */ jsx("input", {
1616
+ ref,
1617
+ type: "checkbox",
1618
+ className: "accent-accent mt-0.5",
1619
+ checked: value === true,
1620
+ onChange: () => onToggle(value !== true)
1621
+ }), /* @__PURE__ */ jsxs("span", {
1622
+ className: "min-w-0",
1623
+ children: [/* @__PURE__ */ jsx("span", {
1624
+ className: "text-fg block text-sm leading-tight",
1625
+ children: label
1626
+ }), /* @__PURE__ */ jsx("span", {
1627
+ className: "text-fg-muted block text-xs leading-tight",
1628
+ children: hint
1629
+ })]
1630
+ })]
1631
+ });
1632
+ }
1633
+ /** The `/F` flags of the current selection, live-editable. Renders nothing
1634
+ * when nothing is selected (the style panel shows its tool defaults then). */
1635
+ function AnnotationFlagsSection() {
1636
+ const annotation = useAnnotation();
1637
+ const flags = useSelectionFlags();
1638
+ if (!flags) return null;
1639
+ return /* @__PURE__ */ jsxs("section", {
1640
+ className: "border-border mt-2 border-t pt-4",
1641
+ children: [/* @__PURE__ */ jsx("p", {
1642
+ className: "text-fg-muted mb-2 text-[11px] font-semibold uppercase tracking-wide",
1643
+ children: "Flags (/F)"
1644
+ }), /* @__PURE__ */ jsx("div", {
1645
+ className: "flex flex-col",
1646
+ children: FLAG_ROWS.map((row) => /* @__PURE__ */ jsx(FlagRow, {
1647
+ label: row.label,
1648
+ hint: row.hint,
1649
+ value: flags[row.key],
1650
+ onToggle: (next) => annotation.updateSelectionFlags({ [row.key]: next })
1651
+ }, row.key))
1652
+ })]
1653
+ });
1654
+ }
1655
+ //#endregion
1656
+ //#region src/ui/annotation-style.tsx
1657
+ /**
1658
+ * The annotation style panel — the v2 snippet's style sidebar, rebuilt the v3
1659
+ * way. The plugin owns the schema AND the state; this file owns only how each
1660
+ * PropSpec renders and the flat patch it writes back. There is NO per-subtype
1661
+ * branching here: a new annotation kind that declares its `PropSpec[]` in the
1662
+ * kind table gets a working style panel for free.
1663
+ *
1664
+ * Data flow (mirrors examples/react's AnnotationSidebar):
1665
+ * selection present → useSelectionProps() specs/values, write updateSelection()
1666
+ * nothing selected → propsForTool(tool) specs + useAnnotationDefaults(tool)
1667
+ * values, write setDefaults(tool, …)
1668
+ *
1669
+ * The look is ported 1:1 from viewers/snippet's annotation-sidebar (six-column
1670
+ * swatch grid, range slider, SVG stroke / line-ending dropdowns, font-size
1671
+ * combo, align toggles), retinted from v2's `bg-bg-input`/`text-fg-primary`
1672
+ * tokens to this app's semantic ones (bg-surface / text-fg / border-border …).
1673
+ */
1674
+ const PRESET_COLORS = [
1675
+ "#000000",
1676
+ "#5f6368",
1677
+ "#e44234",
1678
+ "#ff8b00",
1679
+ "#ffd500",
1680
+ "#00cc66",
1681
+ "#00b8d9",
1682
+ "#4a90e2",
1683
+ "#9b51e0",
1684
+ "#f272c8",
1685
+ "#a0522d",
1686
+ "#ffffff"
1687
+ ];
1688
+ const FONT_OPTIONS = [
1689
+ {
1690
+ v: "helvetica",
1691
+ label: "Helvetica"
1692
+ },
1693
+ {
1694
+ v: "helvetica-bold",
1695
+ label: "Helvetica Bold"
1696
+ },
1697
+ {
1698
+ v: "times-roman",
1699
+ label: "Times Roman"
1700
+ },
1701
+ {
1702
+ v: "courier",
1703
+ label: "Courier"
1704
+ }
1705
+ ];
1706
+ const FONT_SIZES = [
1707
+ 8,
1708
+ 9,
1709
+ 10,
1710
+ 11,
1711
+ 12,
1712
+ 14,
1713
+ 16,
1714
+ 18,
1715
+ 24,
1716
+ 36,
1717
+ 48,
1718
+ 72
1719
+ ];
1720
+ const BORDER_OPTS = [
1721
+ {
1722
+ key: "solid",
1723
+ make: () => ({ kind: "solid" })
1724
+ },
1725
+ {
1726
+ key: "dashed-6-2",
1727
+ make: () => ({
1728
+ kind: "dashed",
1729
+ dash: [6, 2]
1730
+ })
1731
+ },
1732
+ {
1733
+ key: "dashed-3-3",
1734
+ make: () => ({
1735
+ kind: "dashed",
1736
+ dash: [3, 3]
1737
+ })
1738
+ },
1739
+ {
1740
+ key: "dashed-1-2",
1741
+ make: () => ({
1742
+ kind: "dashed",
1743
+ dash: [1, 2]
1744
+ })
1745
+ },
1746
+ {
1747
+ key: "cloudy-1",
1748
+ make: () => ({
1749
+ kind: "cloudy",
1750
+ intensity: 1
1751
+ })
1752
+ },
1753
+ {
1754
+ key: "cloudy-2",
1755
+ make: () => ({
1756
+ kind: "cloudy",
1757
+ intensity: 2
1758
+ })
1759
+ }
1760
+ ];
1761
+ const borderKey = (b) => b.kind === "cloudy" ? `cloudy-${b.intensity >= 2 ? 2 : 1}` : b.kind === "dashed" ? `dashed-${b.dash.join("-")}` : "solid";
1762
+ const LINE_ENDINGS = [
1763
+ {
1764
+ v: "none",
1765
+ label: "None"
1766
+ },
1767
+ {
1768
+ v: "open-arrow",
1769
+ label: "Open arrow"
1770
+ },
1771
+ {
1772
+ v: "closed-arrow",
1773
+ label: "Closed arrow"
1774
+ },
1775
+ {
1776
+ v: "r-open-arrow",
1777
+ label: "Reverse open"
1778
+ },
1779
+ {
1780
+ v: "r-closed-arrow",
1781
+ label: "Reverse closed"
1782
+ },
1783
+ {
1784
+ v: "circle",
1785
+ label: "Circle"
1786
+ },
1787
+ {
1788
+ v: "square",
1789
+ label: "Square"
1790
+ },
1791
+ {
1792
+ v: "diamond",
1793
+ label: "Diamond"
1794
+ },
1795
+ {
1796
+ v: "butt",
1797
+ label: "Butt"
1798
+ },
1799
+ {
1800
+ v: "slash",
1801
+ label: "Slash"
1802
+ }
1803
+ ];
1804
+ const BLEND_MODES = [
1805
+ {
1806
+ v: "normal",
1807
+ label: "Normal"
1808
+ },
1809
+ {
1810
+ v: "multiply",
1811
+ label: "Multiply"
1812
+ },
1813
+ {
1814
+ v: "screen",
1815
+ label: "Screen"
1816
+ },
1817
+ {
1818
+ v: "overlay",
1819
+ label: "Overlay"
1820
+ },
1821
+ {
1822
+ v: "darken",
1823
+ label: "Darken"
1824
+ },
1825
+ {
1826
+ v: "lighten",
1827
+ label: "Lighten"
1828
+ },
1829
+ {
1830
+ v: "color-dodge",
1831
+ label: "Color Dodge"
1832
+ },
1833
+ {
1834
+ v: "color-burn",
1835
+ label: "Color Burn"
1836
+ },
1837
+ {
1838
+ v: "hard-light",
1839
+ label: "Hard Light"
1840
+ },
1841
+ {
1842
+ v: "soft-light",
1843
+ label: "Soft Light"
1844
+ },
1845
+ {
1846
+ v: "difference",
1847
+ label: "Difference"
1848
+ },
1849
+ {
1850
+ v: "exclusion",
1851
+ label: "Exclusion"
1852
+ },
1853
+ {
1854
+ v: "hue",
1855
+ label: "Hue"
1856
+ },
1857
+ {
1858
+ v: "saturation",
1859
+ label: "Saturation"
1860
+ },
1861
+ {
1862
+ v: "color",
1863
+ label: "Color"
1864
+ },
1865
+ {
1866
+ v: "luminosity",
1867
+ label: "Luminosity"
1868
+ }
1869
+ ];
1870
+ function Field({ label, mixed, children }) {
1871
+ return /* @__PURE__ */ jsxs("section", {
1872
+ className: "mb-5",
1873
+ children: [/* @__PURE__ */ jsxs("label", {
1874
+ className: "text-fg mb-2 block text-sm font-medium",
1875
+ children: [label, mixed && /* @__PURE__ */ jsx("span", {
1876
+ className: "text-fg-muted ml-1.5 text-xs font-normal",
1877
+ children: "(mixed)"
1878
+ })]
1879
+ }), children]
1880
+ });
1881
+ }
1882
+ const isTransparent = (c) => c == null || c === "transparent" || /^#([0-9a-f]{8})$/i.test(c) && c.slice(-2).toLowerCase() === "00";
1883
+ function Swatch({ color, active, onSelect }) {
1884
+ const style = isTransparent(color) ? {
1885
+ backgroundColor: "#fff",
1886
+ backgroundImage: "linear-gradient(45deg, transparent 40%, #ef4444 40%, #ef4444 60%, transparent 60%)",
1887
+ backgroundSize: "100% 100%"
1888
+ } : { backgroundColor: color };
1889
+ return /* @__PURE__ */ jsx("button", {
1890
+ type: "button",
1891
+ title: color,
1892
+ onClick: () => onSelect(color),
1893
+ className: `border-border-strong h-5 w-5 rounded-full border ${active ? "outline-accent outline outline-2 outline-offset-2" : ""}`,
1894
+ style
1895
+ });
1896
+ }
1897
+ function SwatchGrid({ value, onSelect, allowTransparent }) {
1898
+ const active = (c) => c.toLowerCase() === (value ?? "").toLowerCase();
1899
+ return /* @__PURE__ */ jsxs("div", {
1900
+ className: "grid grid-cols-6 gap-x-1 gap-y-3",
1901
+ children: [PRESET_COLORS.map((c) => /* @__PURE__ */ jsx(Swatch, {
1902
+ color: c,
1903
+ active: active(c),
1904
+ onSelect
1905
+ }, c)), allowTransparent && /* @__PURE__ */ jsx(Swatch, {
1906
+ color: "transparent",
1907
+ active: isTransparent(value ?? null),
1908
+ onSelect
1909
+ })]
1910
+ });
1911
+ }
1912
+ function RangeSlider({ value, min, max, step, onChange }) {
1913
+ return /* @__PURE__ */ jsx("input", {
1914
+ type: "range",
1915
+ className: "accent-accent mb-1.5 h-1 w-full cursor-pointer",
1916
+ value,
1917
+ min,
1918
+ max,
1919
+ step,
1920
+ onChange: (e) => onChange(parseFloat(e.target.value))
1921
+ });
1922
+ }
1923
+ function useOutsideClose(open, close) {
1924
+ const rootRef = useRef(null);
1925
+ useEffect(() => {
1926
+ if (!open) return;
1927
+ const onDoc = (e) => {
1928
+ if (rootRef.current && !e.composedPath().includes(rootRef.current)) close();
1929
+ };
1930
+ document.addEventListener("mousedown", onDoc);
1931
+ return () => document.removeEventListener("mousedown", onDoc);
1932
+ }, [open, close]);
1933
+ return rootRef;
1934
+ }
1935
+ function DropdownShell({ trigger, children }) {
1936
+ const [open, setOpen] = useState(false);
1937
+ return /* @__PURE__ */ jsxs("div", {
1938
+ ref: useOutsideClose(open, () => setOpen(false)),
1939
+ className: "relative w-full",
1940
+ children: [/* @__PURE__ */ jsxs("button", {
1941
+ type: "button",
1942
+ onClick: () => setOpen((o) => !o),
1943
+ className: "border-border bg-surface text-fg flex w-full items-center justify-between gap-2 rounded border px-3 py-1.5",
1944
+ children: [trigger, /* @__PURE__ */ jsx(Icon, {
1945
+ name: "chevronDown",
1946
+ size: 16,
1947
+ className: "text-fg-secondary shrink-0"
1948
+ })]
1949
+ }), open && /* @__PURE__ */ jsx("div", {
1950
+ className: "border-border bg-elevated absolute z-10 mt-1 max-h-60 w-full overflow-y-auto rounded border p-1 shadow-lg",
1951
+ children: children(() => setOpen(false))
1952
+ })]
1953
+ });
1954
+ }
1955
+ function OptionRow({ selected, onClick, children }) {
1956
+ return /* @__PURE__ */ jsx("button", {
1957
+ type: "button",
1958
+ onClick,
1959
+ className: `hover:bg-hover flex w-full items-center rounded px-2 py-1.5 text-left text-sm ${selected ? "bg-selected text-accent" : "text-fg"}`,
1960
+ children
1961
+ });
1962
+ }
1963
+ function borderSvg(b) {
1964
+ if (b.kind === "cloudy") {
1965
+ const r = b.intensity >= 2 ? 5 : 3;
1966
+ const n = Math.ceil(80 / (r * 2));
1967
+ const step = 80 / n;
1968
+ const baseline = r + 1;
1969
+ const viewH = r * 2 + 2;
1970
+ const parts = [`M 0 ${baseline}`];
1971
+ for (let i = 0; i < n; i++) parts.push(`A ${r} ${r} 0 0 1 ${step * (i + 1)} ${baseline}`);
1972
+ return /* @__PURE__ */ jsx("svg", {
1973
+ width: "80",
1974
+ height: viewH,
1975
+ viewBox: `0 0 80 ${viewH}`,
1976
+ children: /* @__PURE__ */ jsx("path", {
1977
+ d: parts.join(" "),
1978
+ fill: "none",
1979
+ stroke: "currentColor",
1980
+ strokeWidth: "1.5",
1981
+ strokeLinejoin: "round",
1982
+ strokeLinecap: "round"
1983
+ })
1984
+ });
1985
+ }
1986
+ return /* @__PURE__ */ jsx("svg", {
1987
+ width: "80",
1988
+ height: "8",
1989
+ viewBox: "0 0 80 8",
1990
+ children: /* @__PURE__ */ jsx("line", {
1991
+ x1: "0",
1992
+ y1: "4",
1993
+ x2: "80",
1994
+ y2: "4",
1995
+ stroke: "currentColor",
1996
+ strokeWidth: "2",
1997
+ strokeDasharray: b.kind === "dashed" ? b.dash.join(" ") : void 0
1998
+ })
1999
+ });
2000
+ }
2001
+ function BorderSelect({ value, cloudy, onChange }) {
2002
+ const opts = cloudy ? BORDER_OPTS : BORDER_OPTS.filter((o) => !o.key.startsWith("cloudy"));
2003
+ return /* @__PURE__ */ jsx(DropdownShell, {
2004
+ trigger: /* @__PURE__ */ jsx("span", {
2005
+ className: "text-fg-secondary",
2006
+ children: borderSvg(value)
2007
+ }),
2008
+ children: (close) => opts.map((o) => {
2009
+ const b = o.make();
2010
+ return /* @__PURE__ */ jsx(OptionRow, {
2011
+ selected: borderKey(value) === o.key,
2012
+ onClick: () => {
2013
+ onChange(b);
2014
+ close();
2015
+ },
2016
+ children: /* @__PURE__ */ jsx("span", {
2017
+ className: "text-fg-secondary",
2018
+ children: borderSvg(b)
2019
+ })
2020
+ }, o.key);
2021
+ })
2022
+ });
2023
+ }
2024
+ const LE_MARKERS = {
2025
+ square: /* @__PURE__ */ jsx("path", { d: "M68 -4 L76 -4 L76 4 L68 4 Z" }),
2026
+ circle: /* @__PURE__ */ jsx("circle", {
2027
+ cx: "72",
2028
+ cy: "0",
2029
+ r: "4"
2030
+ }),
2031
+ diamond: /* @__PURE__ */ jsx("path", { d: "M72 -5 L77 0 L72 5 L67 0 Z" }),
2032
+ "open-arrow": /* @__PURE__ */ jsx("path", {
2033
+ d: "M67 -5 L77 0 L67 5",
2034
+ fill: "none"
2035
+ }),
2036
+ "closed-arrow": /* @__PURE__ */ jsx("path", { d: "M67 -5 L77 0 L67 5 Z" }),
2037
+ "r-open-arrow": /* @__PURE__ */ jsx("path", {
2038
+ d: "M77 -5 L67 0 L77 5",
2039
+ fill: "none"
2040
+ }),
2041
+ "r-closed-arrow": /* @__PURE__ */ jsx("path", { d: "M77 -5 L67 0 L77 5 Z" }),
2042
+ butt: /* @__PURE__ */ jsx("path", {
2043
+ d: "M72 -5 L72 5",
2044
+ fill: "none"
2045
+ }),
2046
+ slash: /* @__PURE__ */ jsx("path", {
2047
+ d: "M67 -5 L77 5",
2048
+ fill: "none"
2049
+ })
2050
+ };
2051
+ const LE_LINE_END = {
2052
+ square: 68,
2053
+ circle: 68,
2054
+ diamond: 67,
2055
+ "open-arrow": 76,
2056
+ "closed-arrow": 67,
2057
+ "r-open-arrow": 67,
2058
+ "r-closed-arrow": 67,
2059
+ butt: 72,
2060
+ slash: 72
2061
+ };
2062
+ function LineEndingPreview({ name, side }) {
2063
+ const marker = LE_MARKERS[name];
2064
+ const lineEndX = LE_LINE_END[name] ?? 77;
2065
+ return /* @__PURE__ */ jsx("svg", {
2066
+ width: "80",
2067
+ height: "20",
2068
+ viewBox: "0 0 80 20",
2069
+ className: "text-fg",
2070
+ children: /* @__PURE__ */ jsxs("g", {
2071
+ transform: side === "start" ? "rotate(180 40 10)" : void 0,
2072
+ children: [/* @__PURE__ */ jsx("line", {
2073
+ x1: "4",
2074
+ y1: "10",
2075
+ x2: lineEndX,
2076
+ y2: "10",
2077
+ stroke: "currentColor",
2078
+ strokeWidth: "1.5"
2079
+ }), marker && /* @__PURE__ */ jsx("g", {
2080
+ transform: "translate(0, 10)",
2081
+ fill: "currentColor",
2082
+ stroke: "currentColor",
2083
+ strokeWidth: "1.5",
2084
+ children: marker
2085
+ })]
2086
+ })
2087
+ });
2088
+ }
2089
+ function LineEndingSelect({ side, value, onChange }) {
2090
+ return /* @__PURE__ */ jsx(DropdownShell, {
2091
+ trigger: /* @__PURE__ */ jsx(LineEndingPreview, {
2092
+ name: value,
2093
+ side
2094
+ }),
2095
+ children: (close) => LINE_ENDINGS.map((o) => /* @__PURE__ */ jsx(OptionRow, {
2096
+ selected: value === o.v,
2097
+ onClick: () => {
2098
+ onChange(o.v);
2099
+ close();
2100
+ },
2101
+ children: /* @__PURE__ */ jsx(LineEndingPreview, {
2102
+ name: o.v,
2103
+ side
2104
+ })
2105
+ }, o.v))
2106
+ });
2107
+ }
2108
+ function FontFamilySelect({ value, onChange }) {
2109
+ return /* @__PURE__ */ jsx(DropdownShell, {
2110
+ trigger: /* @__PURE__ */ jsx("span", {
2111
+ className: "text-fg text-sm",
2112
+ children: FONT_OPTIONS.find((o) => o.v === value)?.label ?? value
2113
+ }),
2114
+ children: (close) => FONT_OPTIONS.map((o) => /* @__PURE__ */ jsx(OptionRow, {
2115
+ selected: o.v === value,
2116
+ onClick: () => {
2117
+ onChange(o.v);
2118
+ close();
2119
+ },
2120
+ children: o.label
2121
+ }, o.v))
2122
+ });
2123
+ }
2124
+ function FontSizeCombo({ value, onChange }) {
2125
+ const [open, setOpen] = useState(false);
2126
+ return /* @__PURE__ */ jsxs("div", {
2127
+ ref: useOutsideClose(open, () => setOpen(false)),
2128
+ className: "relative w-full",
2129
+ children: [
2130
+ /* @__PURE__ */ jsx("input", {
2131
+ type: "number",
2132
+ min: 1,
2133
+ value,
2134
+ onChange: (e) => {
2135
+ const n = parseInt(e.target.value, 10);
2136
+ if (Number.isFinite(n) && n > 0) onChange(n);
2137
+ },
2138
+ onClick: () => setOpen(true),
2139
+ className: "border-border bg-surface text-fg w-full rounded border px-2 py-1.5 pr-7 text-sm"
2140
+ }),
2141
+ /* @__PURE__ */ jsx("button", {
2142
+ type: "button",
2143
+ tabIndex: -1,
2144
+ onClick: () => setOpen((o) => !o),
2145
+ className: "absolute inset-y-0 right-1 flex items-center",
2146
+ children: /* @__PURE__ */ jsx(Icon, {
2147
+ name: "chevronDown",
2148
+ size: 16,
2149
+ className: "text-fg-secondary"
2150
+ })
2151
+ }),
2152
+ open && /* @__PURE__ */ jsx("div", {
2153
+ className: "border-border bg-elevated absolute z-10 mt-1 max-h-48 w-full overflow-y-auto rounded border p-1 shadow-lg",
2154
+ children: FONT_SIZES.map((sz) => /* @__PURE__ */ jsx(OptionRow, {
2155
+ selected: sz === value,
2156
+ onClick: () => {
2157
+ onChange(sz);
2158
+ setOpen(false);
2159
+ },
2160
+ children: sz
2161
+ }, sz))
2162
+ })
2163
+ ]
2164
+ });
2165
+ }
2166
+ function BlendModeSelect({ value, onChange }) {
2167
+ return /* @__PURE__ */ jsx("select", {
2168
+ value,
2169
+ onChange: (event) => onChange(event.target.value),
2170
+ className: "border-border bg-surface text-fg w-full rounded border px-3 py-1.5 text-sm",
2171
+ children: BLEND_MODES.map((mode) => /* @__PURE__ */ jsx("option", {
2172
+ value: mode.v,
2173
+ children: mode.label
2174
+ }, mode.v))
2175
+ });
2176
+ }
2177
+ function Toggle({ active, title, onClick, children }) {
2178
+ return /* @__PURE__ */ jsx("button", {
2179
+ type: "button",
2180
+ title,
2181
+ onClick,
2182
+ className: `flex h-9 w-9 items-center justify-center rounded border transition-colors ${active ? "border-accent bg-accent text-on-accent" : "border-border bg-surface text-fg hover:bg-hover"}`,
2183
+ children
2184
+ });
2185
+ }
2186
+ /**
2187
+ * The link-target editor — v2's "Link / Go to link / Remove link" menu items,
2188
+ * rebuilt on the schema: any kind whose table declares the `link` spec gets
2189
+ * this control, and every write is the ONE `updateSelection({ link })` path
2190
+ * (the plugin materializes/retargets/deletes the attached children).
2191
+ */
2192
+ function LinkTargetControl({ label, value, mixed, onChange }) {
2193
+ const kernel = useKernel();
2194
+ const link = useOptionalCapability(LinkToken);
2195
+ const [mode, setMode] = useState(value?.kind === "goto" ? "page" : "uri");
2196
+ const [uri, setUri] = useState(value?.kind === "uri" ? value.uri : "");
2197
+ const [pageNo, setPageNo] = useState("1");
2198
+ useEffect(() => {
2199
+ setMode(value?.kind === "goto" ? "page" : "uri");
2200
+ if (value?.kind === "uri") setUri(value.uri);
2201
+ }, [value]);
2202
+ const apply = () => {
2203
+ if (mode === "uri") {
2204
+ const trimmed = uri.trim();
2205
+ if (trimmed) onChange({ link: {
2206
+ kind: "uri",
2207
+ uri: trimmed
2208
+ } });
2209
+ return;
2210
+ }
2211
+ const activeId = kernel.documents.activeId();
2212
+ const layout = (activeId ? kernel.getState().core.documents[activeId] : null)?.pages[Math.max(0, Number(pageNo) - 1)];
2213
+ if (layout) onChange({ link: {
2214
+ kind: "goto",
2215
+ destination: {
2216
+ kind: "fit",
2217
+ pageObjectNumber: layout.pageObjectNumber
2218
+ }
2219
+ } });
2220
+ };
2221
+ const inputCls = "border-border bg-surface text-fg w-full rounded border px-3 py-1.5 text-sm";
2222
+ return /* @__PURE__ */ jsxs(Field, {
2223
+ label,
2224
+ mixed,
2225
+ children: [
2226
+ value != null && /* @__PURE__ */ jsxs("div", {
2227
+ className: "mb-2 flex items-center gap-2",
2228
+ children: [/* @__PURE__ */ jsx("button", {
2229
+ type: "button",
2230
+ className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2231
+ onClick: () => link?.activate(value),
2232
+ children: "Go to link"
2233
+ }), /* @__PURE__ */ jsx("button", {
2234
+ type: "button",
2235
+ className: "border-border bg-surface text-fg flex-1 rounded border px-2 py-1.5 text-sm",
2236
+ onClick: () => onChange({ link: null }),
2237
+ children: "Remove link"
2238
+ })]
2239
+ }),
2240
+ /* @__PURE__ */ jsxs("div", {
2241
+ className: "mb-2 flex gap-2",
2242
+ children: [/* @__PURE__ */ jsx(Toggle, {
2243
+ title: "Link to a URL",
2244
+ active: mode === "uri",
2245
+ onClick: () => setMode("uri"),
2246
+ children: "URL"
2247
+ }), /* @__PURE__ */ jsx(Toggle, {
2248
+ title: "Link to a page",
2249
+ active: mode === "page",
2250
+ onClick: () => setMode("page"),
2251
+ children: "Page"
2252
+ })]
2253
+ }),
2254
+ mode === "uri" ? /* @__PURE__ */ jsx("input", {
2255
+ type: "url",
2256
+ className: inputCls,
2257
+ placeholder: "https://…",
2258
+ value: uri,
2259
+ onChange: (e) => setUri(e.target.value),
2260
+ onKeyDown: (e) => e.key === "Enter" && apply()
2261
+ }) : /* @__PURE__ */ jsx("input", {
2262
+ type: "number",
2263
+ min: 1,
2264
+ className: inputCls,
2265
+ placeholder: "Page number",
2266
+ value: pageNo,
2267
+ onChange: (e) => setPageNo(e.target.value),
2268
+ onKeyDown: (e) => e.key === "Enter" && apply()
2269
+ }),
2270
+ /* @__PURE__ */ jsx("button", {
2271
+ type: "button",
2272
+ className: "border-border bg-surface text-fg mt-2 w-full rounded border px-2 py-1.5 text-sm font-medium",
2273
+ onClick: apply,
2274
+ children: value == null ? "Add link" : "Update link"
2275
+ })
2276
+ ]
2277
+ });
2278
+ }
2279
+ function PropControl({ spec, value, mixed, onChange }) {
2280
+ switch (spec.key) {
2281
+ case "color":
2282
+ case "fontColor": return /* @__PURE__ */ jsx(Field, {
2283
+ label: spec.label,
2284
+ mixed,
2285
+ children: /* @__PURE__ */ jsx(SwatchGrid, {
2286
+ value,
2287
+ onSelect: (c) => onChange({ [spec.key]: c })
2288
+ })
2289
+ });
2290
+ case "interiorColor": return /* @__PURE__ */ jsx(Field, {
2291
+ label: spec.label,
2292
+ mixed,
2293
+ children: /* @__PURE__ */ jsx(SwatchGrid, {
2294
+ value,
2295
+ allowTransparent: true,
2296
+ onSelect: (c) => onChange({ interiorColor: c === "transparent" ? null : c })
2297
+ })
2298
+ });
2299
+ case "opacity": {
2300
+ const v = value ?? 1;
2301
+ return /* @__PURE__ */ jsxs(Field, {
2302
+ label: spec.label,
2303
+ mixed,
2304
+ children: [/* @__PURE__ */ jsx(RangeSlider, {
2305
+ value: v,
2306
+ min: spec.min,
2307
+ max: spec.max,
2308
+ step: spec.step,
2309
+ onChange: (n) => onChange({ opacity: n })
2310
+ }), /* @__PURE__ */ jsxs("span", {
2311
+ className: "text-fg-muted text-xs",
2312
+ children: [Math.round(v * 100), "%"]
2313
+ })]
2314
+ });
2315
+ }
2316
+ case "strokeWidth": {
2317
+ const v = value ?? spec.min;
2318
+ return /* @__PURE__ */ jsxs(Field, {
2319
+ label: spec.label,
2320
+ mixed,
2321
+ children: [/* @__PURE__ */ jsx(RangeSlider, {
2322
+ value: v,
2323
+ min: spec.min,
2324
+ max: spec.max,
2325
+ step: spec.step,
2326
+ onChange: (n) => onChange({ strokeWidth: n })
2327
+ }), /* @__PURE__ */ jsxs("span", {
2328
+ className: "text-fg-muted text-xs",
2329
+ children: [v, "px"]
2330
+ })]
2331
+ });
2332
+ }
2333
+ case "fontSize": return /* @__PURE__ */ jsx(Field, {
2334
+ label: spec.label,
2335
+ mixed,
2336
+ children: /* @__PURE__ */ jsx(FontSizeCombo, {
2337
+ value: value ?? 12,
2338
+ onChange: (n) => onChange({ fontSize: n })
2339
+ })
2340
+ });
2341
+ case "border": return /* @__PURE__ */ jsx(Field, {
2342
+ label: spec.label,
2343
+ mixed,
2344
+ children: /* @__PURE__ */ jsx(BorderSelect, {
2345
+ value: value ?? { kind: "solid" },
2346
+ cloudy: spec.cloudy,
2347
+ onChange: (b) => onChange({ border: b })
2348
+ })
2349
+ });
2350
+ case "lineEndings": {
2351
+ const le = value ?? {
2352
+ start: "none",
2353
+ end: "none"
2354
+ };
2355
+ return /* @__PURE__ */ jsx(Field, {
2356
+ label: spec.label,
2357
+ mixed,
2358
+ children: /* @__PURE__ */ jsxs("div", {
2359
+ className: "grid grid-cols-2 gap-3",
2360
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
2361
+ className: "text-fg-muted mb-1.5 text-xs",
2362
+ children: "Start"
2363
+ }), /* @__PURE__ */ jsx(LineEndingSelect, {
2364
+ side: "start",
2365
+ value: le.start,
2366
+ onChange: (v) => onChange({ lineEndings: { start: v } })
2367
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
2368
+ className: "text-fg-muted mb-1.5 text-xs",
2369
+ children: "End"
2370
+ }), /* @__PURE__ */ jsx(LineEndingSelect, {
2371
+ side: "end",
2372
+ value: le.end,
2373
+ onChange: (v) => onChange({ lineEndings: { end: v } })
2374
+ })] })]
2375
+ })
2376
+ });
2377
+ }
2378
+ case "fontFamily": return /* @__PURE__ */ jsx(Field, {
2379
+ label: spec.label,
2380
+ mixed,
2381
+ children: /* @__PURE__ */ jsx(FontFamilySelect, {
2382
+ value: value ?? "helvetica",
2383
+ onChange: (v) => onChange({ fontFamily: v })
2384
+ })
2385
+ });
2386
+ case "textAlign": {
2387
+ const alignIcon = {
2388
+ left: "alignLeft",
2389
+ center: "alignCenter",
2390
+ right: "alignRight"
2391
+ };
2392
+ return /* @__PURE__ */ jsx(Field, {
2393
+ label: spec.label,
2394
+ mixed,
2395
+ children: /* @__PURE__ */ jsx("div", {
2396
+ className: "flex gap-2",
2397
+ children: [
2398
+ "left",
2399
+ "center",
2400
+ "right"
2401
+ ].map((al) => /* @__PURE__ */ jsx(Toggle, {
2402
+ title: `Align ${al}`,
2403
+ active: value === al,
2404
+ onClick: () => onChange({ textAlign: al }),
2405
+ children: /* @__PURE__ */ jsx(Icon, {
2406
+ name: alignIcon[al],
2407
+ size: 18
2408
+ })
2409
+ }, al))
2410
+ })
2411
+ });
2412
+ }
2413
+ case "blendMode": return /* @__PURE__ */ jsx(Field, {
2414
+ label: spec.label,
2415
+ mixed,
2416
+ children: /* @__PURE__ */ jsx(BlendModeSelect, {
2417
+ value: value ?? "normal",
2418
+ onChange: (blendMode) => onChange({ blendMode })
2419
+ })
2420
+ });
2421
+ case "link": return /* @__PURE__ */ jsx(LinkTargetControl, {
2422
+ label: spec.label,
2423
+ value: value ?? null,
2424
+ mixed,
2425
+ onChange
2426
+ });
2427
+ default: return null;
2428
+ }
2429
+ }
2430
+ function EmptyState() {
2431
+ return /* @__PURE__ */ jsxs("div", {
2432
+ className: "text-fg-muted flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center",
2433
+ children: [/* @__PURE__ */ jsx(Icon, {
2434
+ name: "palette",
2435
+ size: 40,
2436
+ className: "text-fg-disabled"
2437
+ }), /* @__PURE__ */ jsx("p", {
2438
+ className: "max-w-[180px] text-sm",
2439
+ children: "Pick a draw tool or select an annotation to edit its style."
2440
+ })]
2441
+ });
2442
+ }
2443
+ /**
2444
+ * The schema-driven style panel. Rendered inside the right sidebar's
2445
+ * `annotation-style` surface (see ui/panels.tsx). Owns its own scroll.
2446
+ */
2447
+ function AnnotationStylePanel() {
2448
+ const annotation = useAnnotation();
2449
+ const { activeToolId } = useTool();
2450
+ const sel = useSelectionProps();
2451
+ const defaults = useAnnotationDefaults(activeToolId);
2452
+ const selected = useAnnotationSelected();
2453
+ const hasSel = sel.specs.length > 0;
2454
+ const specs = hasSel ? sel.specs : annotation.propsForTool(activeToolId);
2455
+ const values = hasSel ? sel.values : defaults;
2456
+ const write = (patch) => hasSel ? annotation.updateSelection(patch) : annotation.setDefaults(activeToolId, patch);
2457
+ if (specs.length === 0 && selected.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
2458
+ return /* @__PURE__ */ jsxs("div", {
2459
+ className: "flex-1 overflow-y-auto p-4",
2460
+ children: [
2461
+ /* @__PURE__ */ jsx("p", {
2462
+ className: "text-fg-muted mb-4 text-[11px] font-semibold uppercase tracking-wide",
2463
+ children: hasSel ? `${selected.length} selected` : `${activeToolId} defaults`
2464
+ }),
2465
+ specs.map((spec) => /* @__PURE__ */ jsx(PropControl, {
2466
+ spec,
2467
+ value: values[spec.key],
2468
+ mixed: hasSel && sel.mixed.includes(spec.key),
2469
+ onChange: write
2470
+ }, spec.key)),
2471
+ /* @__PURE__ */ jsx(RedactionLabelSection, {}),
2472
+ /* @__PURE__ */ jsx(AnnotationFlagsSection, {})
2473
+ ]
2474
+ });
2475
+ }
2476
+ /**
2477
+ * Label editor shown when exactly one redaction mark is selected. The text is
2478
+ * what the destructive apply paints over the region (white-on-black by the
2479
+ * tool defaults); `repeat` tiles it. Writes ride the annotation update verb
2480
+ * via the redaction plugin, which preserves the label's `/DA` styling.
2481
+ */
2482
+ function RedactionLabelSection() {
2483
+ const t = useT();
2484
+ const redaction = useOptionalCapability(RedactionToken$1);
2485
+ const selected = useAnnotationSelected();
2486
+ const mark = selected.length === 1 && selected[0].subtype === "redact" ? selected[0] : null;
2487
+ const [draft, setDraft] = useState(null);
2488
+ useEffect(() => setDraft(null), [mark?.ref && refKeyOfRedact(mark.ref)]);
2489
+ if (!redaction || !mark || mark.subtype !== "redact") return null;
2490
+ const value = draft ?? mark.overlayText ?? "";
2491
+ const commit = () => {
2492
+ if (draft === null) return;
2493
+ redaction.setLabel(mark.ref, { overlayText: draft.length > 0 ? draft : null });
2494
+ setDraft(null);
2495
+ };
2496
+ return /* @__PURE__ */ jsxs("div", {
2497
+ className: "border-border-subtle mt-4 border-t pt-4",
2498
+ children: [
2499
+ /* @__PURE__ */ jsx("p", {
2500
+ className: "text-fg-muted mb-2 text-[11px] font-semibold uppercase tracking-wide",
2501
+ children: t("demo.redactLabelTitle")
2502
+ }),
2503
+ /* @__PURE__ */ jsx("input", {
2504
+ type: "text",
2505
+ value,
2506
+ placeholder: t("demo.redactLabelPlaceholder"),
2507
+ onChange: (e) => setDraft(e.target.value),
2508
+ onBlur: commit,
2509
+ onKeyDown: (e) => e.key === "Enter" && commit(),
2510
+ className: "border-border bg-surface text-fg w-full rounded-md border px-2 py-1.5 text-sm"
2511
+ }),
2512
+ /* @__PURE__ */ jsxs("label", {
2513
+ className: "text-fg mt-2 flex items-center gap-2 text-sm",
2514
+ children: [/* @__PURE__ */ jsx("input", {
2515
+ type: "checkbox",
2516
+ checked: mark.repeat,
2517
+ onChange: (e) => void redaction.setLabel(mark.ref, { repeat: e.target.checked })
2518
+ }), t("demo.redactLabelRepeat")]
2519
+ })
2520
+ ]
2521
+ });
2522
+ }
2523
+ /** Stable key for the effect dep — mirrors the annotation model's ref keys. */
2524
+ function refKeyOfRedact(ref) {
2525
+ return JSON.stringify(ref);
2526
+ }
2527
+ //#endregion
2528
+ //#region src/ui/redaction-panel.tsx
2529
+ /**
2530
+ * The pending-redactions review panel (right sidebar). The list is a LIVE view
2531
+ * over the annotation plane — deleting a mark here is deleting the annotation.
2532
+ * "Apply all" routes through the confirm modal: the apply is irreversible.
2533
+ */
2534
+ function RedactionPanel() {
2535
+ const t = useT();
2536
+ const redaction = useRedaction();
2537
+ const pending = usePendingRedactions();
2538
+ const anno = useCapability(AnnotationToken$1);
2539
+ const confirm = useSurface("redact-confirm");
2540
+ useEffect(() => {
2541
+ redaction.preparePending();
2542
+ }, []);
2543
+ return /* @__PURE__ */ jsxs("div", {
2544
+ className: "flex min-h-0 flex-1 flex-col",
2545
+ children: [/* @__PURE__ */ jsx("div", {
2546
+ className: "min-h-0 flex-1 overflow-y-auto p-2",
2547
+ children: pending.length === 0 ? /* @__PURE__ */ jsxs("div", {
2548
+ className: "flex flex-col items-center gap-3 px-4 py-10 text-center",
2549
+ children: [/* @__PURE__ */ jsx(Icon, {
2550
+ name: "redactionSidebar",
2551
+ size: 32,
2552
+ className: "text-fg-muted"
2553
+ }), /* @__PURE__ */ jsx("p", {
2554
+ className: "text-fg-muted text-sm",
2555
+ children: t("demo.redactionEmpty")
2556
+ })]
2557
+ }) : /* @__PURE__ */ jsx("ul", {
2558
+ className: "flex flex-col gap-1",
2559
+ children: pending.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("div", {
2560
+ role: "button",
2561
+ tabIndex: 0,
2562
+ onClick: () => anno.select(item.ref),
2563
+ onKeyDown: (e) => e.key === "Enter" && anno.select(item.ref),
2564
+ className: "hover:bg-hover group flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left",
2565
+ children: [
2566
+ /* @__PURE__ */ jsx(Icon, {
2567
+ name: item.kind === "area" ? "redactArea" : "redact",
2568
+ size: 16,
2569
+ className: "text-fg-muted shrink-0"
2570
+ }),
2571
+ /* @__PURE__ */ jsx("span", {
2572
+ className: "text-fg min-w-0 flex-1 truncate text-sm",
2573
+ children: item.overlayText ?? t(item.kind === "area" ? "demo.redactionArea" : "demo.redactionText")
2574
+ }),
2575
+ /* @__PURE__ */ jsx("span", {
2576
+ className: "text-fg-muted shrink-0 text-xs",
2577
+ children: t("demo.pageBadge", { params: { page: item.pageIndex >= 0 ? String(item.pageIndex + 1) : "?" } })
2578
+ }),
2579
+ /* @__PURE__ */ jsx("button", {
2580
+ type: "button",
2581
+ onClick: (e) => {
2582
+ e.stopPropagation();
2583
+ anno.delete(item.ref);
2584
+ },
2585
+ className: "text-fg-muted hover:text-fg grid h-6 w-6 shrink-0 place-items-center rounded opacity-0 group-hover:opacity-100",
2586
+ title: t("demo.redactionRemove"),
2587
+ children: /* @__PURE__ */ jsx(Icon, {
2588
+ name: "trash",
2589
+ size: 14
2590
+ })
2591
+ })
2592
+ ]
2593
+ }) }, item.id))
2594
+ })
2595
+ }), /* @__PURE__ */ jsxs("div", {
2596
+ className: "border-border-subtle flex shrink-0 gap-2 border-t p-3",
2597
+ children: [/* @__PURE__ */ jsx("button", {
2598
+ type: "button",
2599
+ disabled: pending.length === 0,
2600
+ onClick: () => {
2601
+ for (const item of pending) anno.delete(item.ref);
2602
+ },
2603
+ className: "border-border text-fg hover:bg-hover flex-1 rounded-md border px-3 py-1.5 text-sm disabled:opacity-40",
2604
+ children: t("commands.redact.clearAll")
2605
+ }), /* @__PURE__ */ jsx("button", {
2606
+ type: "button",
2607
+ disabled: pending.length === 0 || !redaction.canApply() || redaction.applying,
2608
+ onClick: () => confirm.open(),
2609
+ className: "flex-1 rounded-md bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700 disabled:opacity-40",
2610
+ children: t("commands.redact.applyAll")
2611
+ })]
2612
+ })]
2613
+ });
2614
+ }
2615
+ //#endregion
2616
+ //#region src/ui/search-panel.tsx
2617
+ /**
2618
+ * The search panel — the v2 snippet's search sidebar on the v3 plugin-search
2619
+ * capability. ONE shape end to end: the flat `SearchQuery` {text, regex,
2620
+ * matchCase, wholeWord} is what the engine matches, what the plugin stores
2621
+ * (document-scoped, survives the sidebar closing), and what this box renders.
2622
+ * The panel keeps only a DRAFT of it for keystroke/debounce echo.
2623
+ *
2624
+ * Data flow:
2625
+ * type / toggle → validateSearchQuery → (debounced) search(draftQuery)
2626
+ * useSearchState() → query / status / hitCount / activeIndex (reactive)
2627
+ * useSelector(SearchToken, c => c.hits()) → the streamed hit list
2628
+ * click a hit / prev / next → goTo/prev/next (capability reveals it on-page)
2629
+ *
2630
+ * The look is ported 1:1 from viewers/snippet's search-sidebar (magnifier
2631
+ * input with clear button, option checkboxes, results-found counter with
2632
+ * prev/next, per-page grouped snippet list with the match bolded and the
2633
+ * active hit accented), retinted to this app's semantic tokens. The regex
2634
+ * toggle is new in v3 (v2 had no pattern search); matchDiacritics exists in
2635
+ * the engine but is deliberately not exposed here — v2 parity.
2636
+ */
2637
+ const DEBOUNCE_MS = 300;
2638
+ /** Field-wise equality on the flat query — the mount/echo no-op guard. */
2639
+ const sameQuery = (a, b) => a != null && a.text === b.text && !!a.regex === !!b.regex && !!a.matchCase === !!b.matchCase && !!a.wholeWord === !!b.wholeWord;
2640
+ function Checkbox({ label, checked, onChange }) {
2641
+ return /* @__PURE__ */ jsxs("label", {
2642
+ className: "text-fg-secondary flex cursor-pointer select-none items-center gap-2 text-xs font-medium",
2643
+ children: [/* @__PURE__ */ jsxs("span", {
2644
+ className: "relative flex h-4 w-4 items-center justify-center",
2645
+ children: [/* @__PURE__ */ jsx("input", {
2646
+ type: "checkbox",
2647
+ checked,
2648
+ onChange: (e) => onChange(e.target.checked),
2649
+ className: "border-border bg-surface checked:border-accent checked:bg-accent peer h-4 w-4 shrink-0 appearance-none rounded-[3px] border transition-colors"
2650
+ }), /* @__PURE__ */ jsx("svg", {
2651
+ viewBox: "0 0 24 24",
2652
+ className: "text-on-accent pointer-events-none absolute h-3 w-3 opacity-0 peer-checked:opacity-100",
2653
+ fill: "none",
2654
+ stroke: "currentColor",
2655
+ strokeWidth: 3,
2656
+ strokeLinecap: "round",
2657
+ strokeLinejoin: "round",
2658
+ children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
2659
+ })]
2660
+ }), label]
2661
+ });
2662
+ }
2663
+ function renderSnippet(s) {
2664
+ if (!s) return null;
2665
+ const end = s.matchStart + s.matchLength;
2666
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2667
+ s.text.slice(0, s.matchStart),
2668
+ /* @__PURE__ */ jsx("span", {
2669
+ className: "text-accent font-semibold",
2670
+ children: s.text.slice(s.matchStart, end)
2671
+ }),
2672
+ s.text.slice(end)
2673
+ ] });
2674
+ }
2675
+ function HitLine({ hit, active, onClick }) {
2676
+ const ref = useRef(null);
2677
+ useEffect(() => {
2678
+ if (active) ref.current?.scrollIntoView({
2679
+ behavior: "smooth",
2680
+ block: "nearest"
2681
+ });
2682
+ }, [active]);
2683
+ return /* @__PURE__ */ jsx("button", {
2684
+ ref,
2685
+ type: "button",
2686
+ onClick,
2687
+ className: `w-full rounded border p-2 text-left text-sm transition-colors ${active ? "border-accent bg-accent-light text-fg" : "border-border-subtle bg-surface text-fg-secondary hover:bg-hover"}`,
2688
+ children: /* @__PURE__ */ jsx("span", { children: renderSnippet(hit.snippet) })
2689
+ });
2690
+ }
2691
+ function SearchPanel() {
2692
+ const t = useT();
2693
+ const search = useSearch();
2694
+ const { query, status, hitCount, activeIndex } = useSearchState();
2695
+ const hits = useSelector(SearchToken$1, (c) => c.hits());
2696
+ const [draft, setDraft] = useState(() => query?.text ?? "");
2697
+ const [matchCase, setMatchCase] = useState(() => query?.matchCase ?? false);
2698
+ const [wholeWord, setWholeWord] = useState(() => query?.wholeWord ?? false);
2699
+ const [regex, setRegex] = useState(() => query?.regex ?? false);
2700
+ const inputRef = useRef(null);
2701
+ const draftQuery = {
2702
+ text: draft,
2703
+ regex,
2704
+ matchCase,
2705
+ wholeWord
2706
+ };
2707
+ const validation = draft && regex ? validateSearchQuery(draftQuery) : { ok: true };
2708
+ useEffect(() => {
2709
+ if (!validation.ok || sameQuery(query, draftQuery)) return;
2710
+ if (draft === "" && query === null) return;
2711
+ const id = setTimeout(() => search.search(draftQuery), DEBOUNCE_MS);
2712
+ return () => clearTimeout(id);
2713
+ }, [
2714
+ draft,
2715
+ matchCase,
2716
+ wholeWord,
2717
+ regex,
2718
+ query,
2719
+ validation.ok,
2720
+ search
2721
+ ]);
2722
+ const grouped = useMemo(() => {
2723
+ const map = /* @__PURE__ */ new Map();
2724
+ hits.forEach((hit, index) => {
2725
+ const arr = map.get(hit.pageIndex);
2726
+ if (arr) arr.push({
2727
+ hit,
2728
+ index
2729
+ });
2730
+ else map.set(hit.pageIndex, [{
2731
+ hit,
2732
+ index
2733
+ }]);
2734
+ });
2735
+ return [...map.entries()].sort((a, b) => a[0] - b[0]);
2736
+ }, [hits]);
2737
+ const searching = status === "searching";
2738
+ return /* @__PURE__ */ jsxs("div", {
2739
+ className: "flex min-h-0 flex-1 flex-col",
2740
+ children: [/* @__PURE__ */ jsxs("div", {
2741
+ className: "p-3",
2742
+ children: [
2743
+ /* @__PURE__ */ jsxs("div", {
2744
+ className: "relative",
2745
+ children: [
2746
+ /* @__PURE__ */ jsx("div", {
2747
+ className: "text-fg-muted pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5",
2748
+ children: /* @__PURE__ */ jsx(Icon, {
2749
+ name: "search",
2750
+ size: 16
2751
+ })
2752
+ }),
2753
+ /* @__PURE__ */ jsx("input", {
2754
+ ref: inputRef,
2755
+ type: "text",
2756
+ value: draft,
2757
+ placeholder: t("demo.searchPlaceholder"),
2758
+ onChange: (e) => setDraft(e.target.value),
2759
+ className: `bg-surface text-fg w-full rounded-md border py-1.5 pl-8 pr-9 text-sm outline-none focus:ring-1 ${validation.ok ? "border-border focus:border-accent focus:ring-accent" : "border-red-400 focus:border-red-400 focus:ring-red-400"}`
2760
+ }),
2761
+ draft && /* @__PURE__ */ jsx("button", {
2762
+ type: "button",
2763
+ onClick: () => {
2764
+ setDraft("");
2765
+ inputRef.current?.focus();
2766
+ },
2767
+ className: "text-fg-muted hover:text-fg-secondary absolute inset-y-0 right-0 flex items-center pr-2.5",
2768
+ children: /* @__PURE__ */ jsx(Icon, {
2769
+ name: "x",
2770
+ size: 16
2771
+ })
2772
+ })
2773
+ ]
2774
+ }),
2775
+ !validation.ok && /* @__PURE__ */ jsx("p", {
2776
+ className: "mt-1.5 text-xs text-red-500",
2777
+ children: validation.message
2778
+ }),
2779
+ /* @__PURE__ */ jsxs("div", {
2780
+ className: "mt-3 flex flex-col gap-2",
2781
+ children: [
2782
+ /* @__PURE__ */ jsx(Checkbox, {
2783
+ label: t("demo.searchCaseSensitive"),
2784
+ checked: matchCase,
2785
+ onChange: setMatchCase
2786
+ }),
2787
+ /* @__PURE__ */ jsx(Checkbox, {
2788
+ label: t("demo.searchWholeWord"),
2789
+ checked: wholeWord,
2790
+ onChange: setWholeWord
2791
+ }),
2792
+ /* @__PURE__ */ jsx(Checkbox, {
2793
+ label: t("demo.searchRegex"),
2794
+ checked: regex,
2795
+ onChange: setRegex
2796
+ })
2797
+ ]
2798
+ }),
2799
+ /* @__PURE__ */ jsx("hr", { className: "border-border-subtle mb-2 mt-4" }),
2800
+ status !== "idle" && /* @__PURE__ */ jsxs("div", {
2801
+ className: "flex h-8 items-center justify-between",
2802
+ children: [/* @__PURE__ */ jsx("div", {
2803
+ className: "text-fg-muted text-xs",
2804
+ children: hitCount === 0 ? t(searching ? "demo.searchSearching" : "demo.searchNoResults") : t("demo.searchResults", { params: { count: hitCount } })
2805
+ }), hitCount > 1 && /* @__PURE__ */ jsxs("div", {
2806
+ className: "flex items-center",
2807
+ children: [/* @__PURE__ */ jsx("button", {
2808
+ type: "button",
2809
+ title: "Previous",
2810
+ className: buttonClass(false),
2811
+ onClick: () => search.prev(),
2812
+ children: /* @__PURE__ */ jsx(Icon, {
2813
+ name: "chevronLeft",
2814
+ size: 20
2815
+ })
2816
+ }), /* @__PURE__ */ jsx("button", {
2817
+ type: "button",
2818
+ title: "Next",
2819
+ className: buttonClass(false),
2820
+ onClick: () => search.next(),
2821
+ children: /* @__PURE__ */ jsx(Icon, {
2822
+ name: "chevronRight",
2823
+ size: 20
2824
+ })
2825
+ })]
2826
+ })]
2827
+ })
2828
+ ]
2829
+ }), /* @__PURE__ */ jsx("div", {
2830
+ className: "flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-3 pb-3",
2831
+ children: grouped.map(([pageIndex, items]) => /* @__PURE__ */ jsxs("div", {
2832
+ className: "mt-2 first:mt-0",
2833
+ children: [/* @__PURE__ */ jsx("div", {
2834
+ className: "bg-surface/80 text-fg-muted py-2 text-xs backdrop-blur",
2835
+ children: t("demo.searchPage", { params: { page: pageIndex + 1 } })
2836
+ }), /* @__PURE__ */ jsx("div", {
2837
+ className: "flex flex-col gap-2",
2838
+ children: items.map(({ hit, index }) => /* @__PURE__ */ jsx(HitLine, {
2839
+ hit,
2840
+ active: index === activeIndex,
2841
+ onClick: () => search.goTo(index)
2842
+ }, index))
2843
+ })]
2844
+ }, pageIndex))
2845
+ })]
2846
+ });
2847
+ }
2848
+ //#endregion
2849
+ //#region src/ui/panels.tsx
2850
+ /**
2851
+ * The static chrome around the toolbars: the left/right sidebars (shell
2852
+ * surfaces) and the bottom page-controls overlay. Deliberately minimal —
2853
+ * chrome-parity scope. Panels read their open state from plugin-shell; the
2854
+ * app owns their DOM.
2855
+ *
2856
+ * There is no built-in header: branding, a locale picker and a theme switch
2857
+ * are the EMBEDDER's chrome, not the viewer's. The frame keeps a `header`
2858
+ * socket for a slotted one (see Shell).
2859
+ */
2860
+ function LeftSidebar() {
2861
+ const t = useT();
2862
+ const sidebar = useSurface("sidebar");
2863
+ if (!sidebar.isOpen) return null;
2864
+ return /* @__PURE__ */ jsxs("aside", {
2865
+ className: "border-border-subtle bg-surface flex w-60 shrink-0 flex-col border-r",
2866
+ children: [/* @__PURE__ */ jsxs("div", {
2867
+ className: "border-border-subtle flex items-center gap-1 border-b p-2",
2868
+ children: [
2869
+ /* @__PURE__ */ jsx("span", {
2870
+ className: "bg-accent-light text-accent rounded-md px-2 py-1 text-xs font-medium",
2871
+ children: t("demo.thumbnails")
2872
+ }),
2873
+ /* @__PURE__ */ jsx("span", {
2874
+ className: "text-fg-muted rounded-md px-2 py-1 text-xs font-medium",
2875
+ children: t("demo.outline")
2876
+ }),
2877
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
2878
+ /* @__PURE__ */ jsx("button", {
2879
+ type: "button",
2880
+ onClick: sidebar.close,
2881
+ className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
2882
+ children: /* @__PURE__ */ jsx(Icon, {
2883
+ name: "x",
2884
+ size: 16
2885
+ })
2886
+ })
2887
+ ]
2888
+ }), /* @__PURE__ */ jsx(ThumbnailList, {})]
2889
+ });
2890
+ }
2891
+ function ThumbnailList() {
2892
+ const { currentPage, goToPage } = usePages();
2893
+ const { reveal } = usePages(ThumbsStageToken);
2894
+ useEffect(() => reveal(currentPage), [currentPage, reveal]);
2895
+ return /* @__PURE__ */ jsx(Stage, {
2896
+ token: ThumbsStageToken,
2897
+ zoomGestures: false,
2898
+ className: "flex-1",
2899
+ style: { position: "relative" },
2900
+ pageChrome: (page) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2901
+ type: "button",
2902
+ onClick: () => goToPage(page.pageIndex),
2903
+ title: `Page ${page.pageIndex + 1}`,
2904
+ style: {
2905
+ position: "absolute",
2906
+ top: page.frame.top,
2907
+ left: page.frame.left,
2908
+ right: page.frame.right,
2909
+ bottom: page.frame.bottom,
2910
+ cursor: "pointer",
2911
+ boxSizing: "border-box",
2912
+ borderRadius: 4,
2913
+ border: page.pageIndex === currentPage ? "2px solid var(--ep-accent)" : "1px solid var(--ep-border-subtle)",
2914
+ boxShadow: page.pageIndex === currentPage ? "0 0 0 2px var(--ep-accent-light)" : "none"
2915
+ }
2916
+ }), /* @__PURE__ */ jsx("div", {
2917
+ style: {
2918
+ position: "absolute",
2919
+ bottom: 0,
2920
+ left: 0,
2921
+ right: 0,
2922
+ height: page.frame.bottom,
2923
+ display: "flex",
2924
+ alignItems: "center",
2925
+ justifyContent: "center",
2926
+ fontSize: 11,
2927
+ color: "var(--ep-fg-muted)",
2928
+ pointerEvents: "none"
2929
+ },
2930
+ children: page.pageIndex + 1
2931
+ })] }),
2932
+ children: () => /* @__PURE__ */ jsx(RenderLayer, {})
2933
+ });
2934
+ }
2935
+ function RightSidebar() {
2936
+ const t = useT();
2937
+ const documentId = useDocumentId();
2938
+ const search = useSurface("search");
2939
+ const comment = useSurface("comment");
2940
+ const style = useSurface("annotation-style");
2941
+ const redaction = useSurface("redaction");
2942
+ const active = search.isOpen ? "search" : comment.isOpen ? "comment" : style.isOpen ? "style" : redaction.isOpen ? "redaction" : null;
2943
+ if (!active) return null;
2944
+ const titleKey = active === "search" ? "demo.searchTitle" : active === "comment" ? "demo.commentsTitle" : active === "redaction" ? "demo.redactionTitle" : "demo.styleTitle";
2945
+ const close = active === "search" ? search.close : active === "comment" ? comment.close : active === "redaction" ? redaction.close : style.close;
2946
+ return /* @__PURE__ */ jsxs("aside", {
2947
+ className: "border-border-subtle bg-surface flex w-72 shrink-0 flex-col border-l",
2948
+ children: [/* @__PURE__ */ jsxs("div", {
2949
+ className: "border-border-subtle flex items-center justify-between border-b p-3",
2950
+ children: [/* @__PURE__ */ jsx("span", {
2951
+ className: "text-fg text-sm font-semibold",
2952
+ children: t(titleKey)
2953
+ }), /* @__PURE__ */ jsx("button", {
2954
+ type: "button",
2955
+ onClick: close,
2956
+ className: "text-fg-muted hover:bg-hover grid h-7 w-7 place-items-center rounded-md",
2957
+ children: /* @__PURE__ */ jsx(Icon, {
2958
+ name: "x",
2959
+ size: 16
2960
+ })
2961
+ })]
2962
+ }), active === "style" ? /* @__PURE__ */ jsx(AnnotationStylePanel, {}) : active === "search" ? /* @__PURE__ */ jsx(SearchPanel, {}, documentId ?? "none") : active === "redaction" ? /* @__PURE__ */ jsx(RedactionPanel, {}) : /* @__PURE__ */ jsx("div", {
2963
+ className: "p-3",
2964
+ children: /* @__PURE__ */ jsx("p", {
2965
+ className: "text-fg-muted text-sm",
2966
+ children: t("demo.empty")
2967
+ })
2968
+ })]
2969
+ });
2970
+ }
2971
+ function PageControls() {
2972
+ const t = useT();
2973
+ const current = useSelector(StageToken$1, (c) => c.currentPage() + 1);
2974
+ const total = useSelector(StageToken$1, (c) => c.pageCount());
2975
+ if (!total) return null;
2976
+ return /* @__PURE__ */ jsx("div", {
2977
+ className: "pointer-events-none absolute inset-x-0 bottom-5 flex justify-center",
2978
+ children: /* @__PURE__ */ jsx("div", {
2979
+ className: "border-border-subtle bg-elevated/95 text-fg-secondary pointer-events-auto rounded-full border px-4 py-1.5 text-sm shadow-lg backdrop-blur",
2980
+ children: t("demo.page", { params: {
2981
+ current,
2982
+ total
2983
+ } })
2984
+ })
2985
+ });
2986
+ }
2987
+ //#endregion
2988
+ //#region src/ui/redact-confirm.tsx
2989
+ /**
2990
+ * The apply-all confirm modal (`redact-confirm` shell surface, `exclusive:
2991
+ * 'modal'` via the command router). Shows the pending count and the
2992
+ * CLIENT-SIDE collateral estimate — other annotations the apply will destroy —
2993
+ * before anything irreversible happens. The authoritative collateral count
2994
+ * comes back on the result.
2995
+ */
2996
+ function RedactConfirmModal() {
2997
+ const t = useT();
2998
+ const surface = useSurface("redact-confirm");
2999
+ const redaction = useRedaction();
3000
+ const [busy, setBusy] = useState(false);
3001
+ const [error, setError] = useState(null);
3002
+ if (!surface.isOpen) return null;
3003
+ const count = redaction.pendingCount();
3004
+ const collateral = redaction.estimateCollateral();
3005
+ const apply = async () => {
3006
+ setBusy(true);
3007
+ setError(null);
3008
+ try {
3009
+ await redaction.applyAll();
3010
+ surface.close();
3011
+ } catch (err) {
3012
+ setError(err instanceof Error ? err.message : String(err));
3013
+ } finally {
3014
+ setBusy(false);
3015
+ }
3016
+ };
3017
+ return /* @__PURE__ */ jsx("div", {
3018
+ className: "fixed inset-0 z-50 grid place-items-center bg-black/40",
3019
+ children: /* @__PURE__ */ jsxs("div", {
3020
+ className: "border-border bg-surface w-96 rounded-lg border p-4 shadow-xl",
3021
+ children: [
3022
+ /* @__PURE__ */ jsx("h2", {
3023
+ className: "text-fg text-base font-semibold",
3024
+ children: t("demo.redactConfirmTitle")
3025
+ }),
3026
+ /* @__PURE__ */ jsx("p", {
3027
+ className: "text-fg-muted mt-2 text-sm",
3028
+ children: t("demo.redactConfirmBody", { params: { count } })
3029
+ }),
3030
+ collateral > 0 ? /* @__PURE__ */ jsx("p", {
3031
+ className: "mt-2 text-sm font-medium text-red-600",
3032
+ children: t("demo.redactConfirmCollateral", { params: { count: collateral } })
3033
+ }) : null,
3034
+ error ? /* @__PURE__ */ jsx("p", {
3035
+ className: "mt-2 text-sm text-red-600",
3036
+ children: error
3037
+ }) : null,
3038
+ /* @__PURE__ */ jsxs("div", {
3039
+ className: "mt-4 flex justify-end gap-2",
3040
+ children: [/* @__PURE__ */ jsx("button", {
3041
+ type: "button",
3042
+ disabled: busy,
3043
+ onClick: surface.close,
3044
+ className: "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm",
3045
+ children: t("demo.cancel")
3046
+ }), /* @__PURE__ */ jsx("button", {
3047
+ type: "button",
3048
+ disabled: busy,
3049
+ onClick: () => void apply(),
3050
+ className: "rounded-md bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700 disabled:opacity-60",
3051
+ children: busy ? t("demo.redactApplying") : t("demo.redactApply")
3052
+ })]
3053
+ })
3054
+ ]
3055
+ })
3056
+ });
3057
+ }
3058
+ //#endregion
3059
+ //#region src/ui/document-boot.tsx
3060
+ /**
3061
+ * Boot-state panes for the document area — what renders INSTEAD of the Stage
3062
+ * while the selected tab's document isn't ready:
3063
+ *
3064
+ * `locked` → <PasswordPrompt/> (per-tab, v2's document-password-prompt port)
3065
+ * `error` → <DocumentError/>
3066
+ *
3067
+ * Both are per-TAB, not modal: several tabs can be locked/broken at once and
3068
+ * switching tabs stays free. The prompt drives ONE call —
3069
+ * `documents.unlock(id, { password })` — identical on the local (worker loads
3070
+ * the parked bytes) and cloud (/access grant) engines. The "incorrect" copy
3071
+ * keys on a real rejection: either this session's failed attempt, or
3072
+ * `passwordProvided` (a config-supplied password the engine already rejected).
3073
+ */
3074
+ function PasswordPrompt() {
3075
+ const docId = useDocumentId();
3076
+ const { docs, unlock, close } = useDocuments();
3077
+ const t = useT();
3078
+ const [password, setPassword] = useState("");
3079
+ const [rejected, setRejected] = useState(false);
3080
+ const [busy, setBusy] = useState(false);
3081
+ const doc = docs.find((d) => d.id === docId);
3082
+ if (!docId || !doc) return null;
3083
+ const incorrect = rejected || doc.passwordProvided === true;
3084
+ const submit = async () => {
3085
+ if (busy || !password.trim()) return;
3086
+ setBusy(true);
3087
+ try {
3088
+ await unlock(docId, { password });
3089
+ } catch {
3090
+ setRejected(true);
3091
+ setPassword("");
3092
+ } finally {
3093
+ setBusy(false);
3094
+ }
3095
+ };
3096
+ return /* @__PURE__ */ jsx("div", {
3097
+ className: "bg-app flex h-full w-full items-center justify-center p-8",
3098
+ children: /* @__PURE__ */ jsxs("div", {
3099
+ className: "bg-surface border-border w-full max-w-sm rounded-xl border p-6 shadow-lg",
3100
+ children: [
3101
+ /* @__PURE__ */ jsxs("div", {
3102
+ className: "flex flex-col items-center text-center",
3103
+ children: [
3104
+ /* @__PURE__ */ jsx("div", {
3105
+ className: "bg-accent-light mb-4 rounded-full p-4",
3106
+ children: /* @__PURE__ */ jsx(Icon, {
3107
+ name: "lock",
3108
+ size: 28,
3109
+ className: "text-accent"
3110
+ })
3111
+ }),
3112
+ /* @__PURE__ */ jsx("h3", {
3113
+ className: "text-fg text-lg font-semibold",
3114
+ children: t("passwordPrompt.title")
3115
+ }),
3116
+ doc.name && /* @__PURE__ */ jsx("p", {
3117
+ className: "text-fg-muted mt-1 text-sm",
3118
+ children: doc.name
3119
+ })
3120
+ ]
3121
+ }),
3122
+ /* @__PURE__ */ jsx("p", {
3123
+ className: "text-fg-secondary mt-4 text-center text-sm",
3124
+ children: incorrect ? t("passwordPrompt.incorrect") : t("passwordPrompt.required")
3125
+ }),
3126
+ /* @__PURE__ */ jsxs("div", {
3127
+ className: "mt-5",
3128
+ children: [/* @__PURE__ */ jsx("label", {
3129
+ className: "text-fg mb-1.5 block text-sm font-medium",
3130
+ htmlFor: "doc-password",
3131
+ children: t("passwordPrompt.label")
3132
+ }), /* @__PURE__ */ jsx("input", {
3133
+ id: "doc-password",
3134
+ type: "password",
3135
+ value: password,
3136
+ onChange: (e) => setPassword(e.target.value),
3137
+ onKeyDown: (e) => e.key === "Enter" && void submit(),
3138
+ disabled: busy,
3139
+ placeholder: t("passwordPrompt.placeholder"),
3140
+ className: "bg-surface border-border focus:border-accent focus:ring-accent text-fg placeholder:text-fg-muted block w-full rounded-md border px-3 py-2.5 text-sm focus:outline-none focus:ring-1 disabled:opacity-50",
3141
+ autoFocus: true
3142
+ })]
3143
+ }),
3144
+ incorrect && /* @__PURE__ */ jsxs("div", {
3145
+ className: "mt-3 flex items-center gap-2 rounded-md border border-red-500/40 bg-red-500/10 p-3",
3146
+ children: [/* @__PURE__ */ jsx(Icon, {
3147
+ name: "alertTriangle",
3148
+ size: 16,
3149
+ className: "flex-shrink-0 text-red-500"
3150
+ }), /* @__PURE__ */ jsx("p", {
3151
+ className: "text-sm text-red-500",
3152
+ children: t("passwordPrompt.incorrectWarning")
3153
+ })]
3154
+ }),
3155
+ /* @__PURE__ */ jsxs("div", {
3156
+ className: "mt-6 flex gap-3",
3157
+ children: [/* @__PURE__ */ jsx("button", {
3158
+ type: "button",
3159
+ onClick: () => void close(docId),
3160
+ disabled: busy,
3161
+ className: "border-border text-fg-secondary hover:bg-hover flex-1 cursor-pointer rounded-md border px-4 py-2.5 text-sm font-medium transition-colors disabled:opacity-50",
3162
+ children: t("passwordPrompt.cancel")
3163
+ }), /* @__PURE__ */ jsx("button", {
3164
+ type: "button",
3165
+ onClick: () => void submit(),
3166
+ disabled: busy || !password.trim(),
3167
+ className: "bg-accent text-on-accent flex-1 cursor-pointer rounded-md px-4 py-2.5 text-sm font-medium transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",
3168
+ children: busy ? t("passwordPrompt.opening") : t("passwordPrompt.open")
3169
+ })]
3170
+ })
3171
+ ]
3172
+ })
3173
+ });
3174
+ }
3175
+ function DocumentError() {
3176
+ const docId = useDocumentId();
3177
+ const { docs, close } = useDocuments();
3178
+ const t = useT();
3179
+ const doc = docs.find((d) => d.id === docId);
3180
+ if (!docId || !doc) return null;
3181
+ return /* @__PURE__ */ jsx("div", {
3182
+ className: "bg-app flex h-full w-full items-center justify-center p-8",
3183
+ children: /* @__PURE__ */ jsxs("div", {
3184
+ className: "bg-surface border-border flex max-w-sm flex-col items-center rounded-xl border p-6 text-center shadow-lg",
3185
+ children: [
3186
+ /* @__PURE__ */ jsx("div", {
3187
+ className: "mb-4 rounded-full bg-red-500/10 p-4",
3188
+ children: /* @__PURE__ */ jsx(Icon, {
3189
+ name: "alertTriangle",
3190
+ size: 28,
3191
+ className: "text-red-500"
3192
+ })
3193
+ }),
3194
+ /* @__PURE__ */ jsx("h3", {
3195
+ className: "text-fg text-lg font-semibold",
3196
+ children: t("documentError.title")
3197
+ }),
3198
+ doc.name && /* @__PURE__ */ jsx("p", {
3199
+ className: "text-fg-muted mt-1 text-sm",
3200
+ children: doc.name
3201
+ }),
3202
+ /* @__PURE__ */ jsx("p", {
3203
+ className: "text-fg-secondary mt-2 text-sm",
3204
+ children: t("documentError.unknown")
3205
+ }),
3206
+ /* @__PURE__ */ jsx("button", {
3207
+ type: "button",
3208
+ onClick: () => void close(docId),
3209
+ className: "bg-accent text-on-accent mt-5 w-full cursor-pointer rounded-md px-4 py-2.5 text-sm font-medium transition-colors hover:opacity-90",
3210
+ children: t("documentError.close")
3211
+ })
3212
+ ]
3213
+ })
3214
+ });
3215
+ }
3216
+ //#endregion
3217
+ //#region src/Shell.tsx
3218
+ /**
3219
+ * The viewer shell — the snippet's layout, driven entirely by the v3 commands
3220
+ * + measured-toolbar system.
3221
+ *
3222
+ * ── header socket (empty unless a child fills it) ─────────
3223
+ * ── main toolbar (measured; auto-overflows) ───────────────
3224
+ * ── mode band (DERIVED from the shell's open mode surface) ─
3225
+ * left sidebar │ Stage (pages) │ right sidebar
3226
+ * ──────────── page-controls overlay ──────────────────────
3227
+ *
3228
+ * The workspace/document split is structural: header + toolbars are
3229
+ * workspace-scoped and render at t≈0 (translated, measured) while the wasm
3230
+ * engine still boots; everything document-scoped sits inside <DocumentGate>,
3231
+ * whose fallback is the empty-workspace state (v2's loader, but UNDER a live
3232
+ * toolbar instead of replacing the whole app).
3233
+ *
3234
+ * Which mode band shows is not stored anywhere: it's a projection of
3235
+ * plugin-shell's exclusive 'mode' surface, read null-safely so the band simply
3236
+ * doesn't exist without a document.
3237
+ */
3238
+ const ANNOTATION_RENDERERS = [formWidgetRenderer];
3239
+ function ModeBand({ edge }) {
3240
+ const schema = useChromeSchema();
3241
+ const modeSurfaces = useMemo(() => Object.keys(schema.modeBars ?? {}), [schema]);
3242
+ const activeMode = useOptionalSelector(ShellToken$1, (s) => modeSurfaces.find((m) => s.isOpen(m)) ?? null, null);
3243
+ if (!activeMode) return null;
3244
+ const bar = getModeBar(schema, activeMode);
3245
+ if (!bar) return null;
3246
+ return /* @__PURE__ */ jsx("div", {
3247
+ className: `border-border bg-surface-alt flex shrink-0 items-center px-4 py-2 ${edge === "top" ? "border-b" : "border-t"}`,
3248
+ children: /* @__PURE__ */ jsx(AppToolbar, {
3249
+ bar,
3250
+ className: "w-full"
3251
+ })
3252
+ });
3253
+ }
3254
+ /** The empty-workspace state: shown while the initial documents (and the
3255
+ * engine behind them) are still loading — or if the user closes every tab. */
3256
+ function OpeningDocuments() {
3257
+ const t = useT();
3258
+ return /* @__PURE__ */ jsx("div", {
3259
+ className: "grid h-full w-full place-items-center",
3260
+ children: /* @__PURE__ */ jsxs("div", {
3261
+ className: "text-fg-muted flex flex-col items-center gap-3",
3262
+ children: [/* @__PURE__ */ jsx("div", { className: "border-border-subtle border-t-accent h-8 w-8 animate-spin rounded-full border-2" }), /* @__PURE__ */ jsx("div", {
3263
+ className: "text-sm",
3264
+ children: t("demo.opening")
3265
+ })]
3266
+ })
3267
+ });
3268
+ }
3269
+ /**
3270
+ * The document area's lifecycle switch. A `locked` tab shows its password
3271
+ * prompt, an `error` tab its error pane — per TAB (keyed by document), so
3272
+ * several can coexist and tab switching stays free. Everything else is the
3273
+ * ready-gated Stage; the gate's fallback covers `loading`.
3274
+ */
3275
+ function DocumentArea({ children }) {
3276
+ const docId = useDocumentId();
3277
+ const status = useDocumentStatus();
3278
+ if (status === "locked") return /* @__PURE__ */ jsx(PasswordPrompt, {}, docId);
3279
+ if (status === "error") return /* @__PURE__ */ jsx(DocumentError, {}, docId);
3280
+ return /* @__PURE__ */ jsx(DocumentGate, {
3281
+ fallback: /* @__PURE__ */ jsx(OpeningDocuments, {}),
3282
+ children
3283
+ });
3284
+ }
3285
+ function Shell() {
3286
+ useCommandShortcuts();
3287
+ useFilePickerProvider();
3288
+ useFormScriptingProvider();
3289
+ const schema = useChromeSchema();
3290
+ const frame = schema.frame ?? {};
3291
+ const toolbarEdge = frame.toolbar ?? "top";
3292
+ const toolbarBand = schema.bars.main && /* @__PURE__ */ jsx("div", {
3293
+ part: "toolbar",
3294
+ className: `border-border bg-surface flex shrink-0 items-center px-4 py-2 ${toolbarEdge === "top" ? "border-b" : "border-t"}`,
3295
+ children: /* @__PURE__ */ jsx(AppToolbar, {
3296
+ bar: schema.bars.main,
3297
+ className: "w-full"
3298
+ })
3299
+ });
3300
+ return /* @__PURE__ */ jsxs("div", {
3301
+ className: "bg-app text-fg flex h-full flex-col",
3302
+ children: [
3303
+ (frame.header ?? true) && /* @__PURE__ */ jsx("slot", { name: "header" }),
3304
+ (frame.tabs ?? "always") !== "never" && /* @__PURE__ */ jsx("slot", {
3305
+ name: "tabs",
3306
+ children: /* @__PURE__ */ jsx(TabBar, { visibility: frame.tabs ?? "always" })
3307
+ }),
3308
+ toolbarEdge === "top" && /* @__PURE__ */ jsxs(Fragment$1, { children: [toolbarBand, /* @__PURE__ */ jsx(ModeBand, { edge: "top" })] }),
3309
+ /* @__PURE__ */ jsx("div", {
3310
+ className: "relative flex min-h-0 flex-1",
3311
+ children: /* @__PURE__ */ jsxs(DocumentArea, { children: [
3312
+ /* @__PURE__ */ jsx(LeftSidebar, {}),
3313
+ /* @__PURE__ */ jsxs("div", {
3314
+ className: "relative min-w-0 flex-1",
3315
+ children: [
3316
+ /* @__PURE__ */ jsx(ArmedToolCursor, {}),
3317
+ /* @__PURE__ */ jsx(Stage, {
3318
+ interaction: true,
3319
+ overlay: /* @__PURE__ */ jsxs(Fragment$1, { children: [
3320
+ /* @__PURE__ */ jsx(AnnotationStrip, {}),
3321
+ /* @__PURE__ */ jsx(Scrollbar, { axis: "y" }),
3322
+ /* @__PURE__ */ jsx(Scrollbar, { axis: "x" })
3323
+ ] }),
3324
+ className: "h-full w-full",
3325
+ style: { background: "var(--ep-canvas)" },
3326
+ children: () => /* @__PURE__ */ jsxs(Fragment$1, { children: [
3327
+ /* @__PURE__ */ jsx(RenderLayer, { annotations: false }),
3328
+ /* @__PURE__ */ jsx(TileLayer, { annotations: false }),
3329
+ /* @__PURE__ */ jsx(SelectionLayer, {}),
3330
+ /* @__PURE__ */ jsx(SearchLayer, {}),
3331
+ /* @__PURE__ */ jsx(LinkLayer, {}),
3332
+ /* @__PURE__ */ jsx(AnnotationLayer, { renderers: ANNOTATION_RENDERERS })
3333
+ ] })
3334
+ }),
3335
+ /* @__PURE__ */ jsx(PageControls, {})
3336
+ ]
3337
+ }),
3338
+ /* @__PURE__ */ jsx(RightSidebar, {}),
3339
+ /* @__PURE__ */ jsx(RedactConfirmModal, {})
3340
+ ] })
3341
+ }),
3342
+ toolbarEdge === "bottom" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ModeBand, { edge: "bottom" }), toolbarBand] })
3343
+ ]
3344
+ });
3345
+ }
3346
+ //#endregion
3347
+ //#region src/viewer.tsx
3348
+ /**
3349
+ * The full viewer, as a component — and the customization CONTRACT. The
3350
+ * `ViewerCustomization` surface here is the exact shape that later powers
3351
+ * `EmbedPDF.init({...})` and every framework wrapper, because all of it is
3352
+ * plain data (see README.md).
3353
+ *
3354
+ * Two semantics, on purpose:
3355
+ * - REGISTRIES (commands / icons / strings) merge ADDITIVELY over the
3356
+ * defaults by id; a colliding id overrides.
3357
+ * - STRUCTURE (chrome) is an owned VALUE: the default, a transform of it,
3358
+ * or the host's own schema — never merged.
3359
+ *
3360
+ * The engine stays a prop — choosing local-wasm vs cloud is the host's
3361
+ * decision, and each delivery wires its own. The chrome —
3362
+ * toolbars, translations, theme — renders at t≈0; only the pages wait on
3363
+ * the engine. Customization is mount-time (exactly like v2's init config);
3364
+ * changing these props on a mounted viewer does not rebuild the workspace.
3365
+ */
3366
+ /** Lazy built-in packs — a strings override for one of these wraps its loader. */
3367
+ const BUILTIN_LOADERS = { es: () => import("./es-D0PzYE9Z.js").then((m) => m.es) };
3368
+ const isDict = (v) => typeof v === "object" && v !== null;
3369
+ /**
3370
+ * Deep-merge `over` into `base` (immutably), expanding dotted keys —
3371
+ * `{ 'acme.send': 'X' }` and `{ acme: { send: 'X' } }` are the same override.
3372
+ */
3373
+ function mergeTranslations(base, over) {
3374
+ const out = { ...base };
3375
+ for (const [key, value] of Object.entries(over)) {
3376
+ const path = key.split(".");
3377
+ let node = out;
3378
+ for (let i = 0; i < path.length - 1; i++) {
3379
+ const prev = node[path[i]];
3380
+ node = node[path[i]] = isDict(prev) ? { ...prev } : {};
3381
+ }
3382
+ const leaf = path[path.length - 1];
3383
+ const prev = node[leaf];
3384
+ node[leaf] = typeof value === "string" ? value : mergeTranslations(isDict(prev) ? prev : {}, value);
3385
+ }
3386
+ return out;
3387
+ }
3388
+ const mergeLocale = (pack, over) => over ? {
3389
+ ...pack,
3390
+ translations: mergeTranslations(pack.translations, over)
3391
+ } : pack;
3392
+ /** Normalize the shorthand — deliveries use this to read one shape. */
3393
+ const themeConfigOf = (theme) => typeof theme === "string" ? { preference: theme } : theme ?? {};
3394
+ /** Inside <Viewer>: mints the handle from the live kernel, reports it up. */
3395
+ function HandleBridge({ onViewer }) {
3396
+ const kernel = useKernel();
3397
+ useEffect(() => {
3398
+ onViewer(createViewerHandle(kernel));
3399
+ }, [kernel]);
3400
+ return null;
3401
+ }
3402
+ function Booting() {
3403
+ return /* @__PURE__ */ jsx("div", {
3404
+ className: "bg-app text-fg-muted grid h-full place-items-center",
3405
+ children: /* @__PURE__ */ jsx("div", {
3406
+ className: "animate-pulse text-sm",
3407
+ children: useT()("demo.starting")
3408
+ })
3409
+ });
3410
+ }
3411
+ function FullViewer({ engine, initialDocuments, fallback, commands, icons, strings, locale = "auto", disabledCategories, chrome, theme, themeTarget, onViewer }) {
3412
+ const [resolved] = useState(() => {
3413
+ const byId = new Map(defaultCommands.map((c) => [c.id, c]));
3414
+ for (const c of commands ?? []) byId.set(c.id, c);
3415
+ const resolvedCommands = [...byId.values()];
3416
+ const resolvedChrome = typeof chrome === "function" ? chrome(defaultChrome, chromeHelpers$1) : chrome ?? defaultChrome;
3417
+ const overrides = strings ?? {};
3418
+ const locales = [mergeLocale(en, overrides.en)];
3419
+ const loaders = {};
3420
+ for (const [code, load] of Object.entries(BUILTIN_LOADERS)) {
3421
+ const over = overrides[code];
3422
+ loaders[code] = over ? () => load().then((pack) => mergeLocale(pack, over)) : load;
3423
+ }
3424
+ for (const [code, over] of Object.entries(overrides)) {
3425
+ if (code === "en" || code in BUILTIN_LOADERS) continue;
3426
+ locales.push({
3427
+ code,
3428
+ name: code,
3429
+ translations: mergeTranslations({}, over)
3430
+ });
3431
+ }
3432
+ const codes = [.../* @__PURE__ */ new Set([
3433
+ "en",
3434
+ ...Object.keys(BUILTIN_LOADERS),
3435
+ ...Object.keys(overrides)
3436
+ ])];
3437
+ const initial = locale === "auto" ? negotiateLocale(codes, typeof navigator !== "undefined" ? navigator.languages : []) ?? "en" : locale;
3438
+ return {
3439
+ commands: resolvedCommands,
3440
+ chrome: resolvedChrome,
3441
+ icons: icons ?? {},
3442
+ i18n: {
3443
+ locales,
3444
+ loaders,
3445
+ initial
3446
+ }
3447
+ };
3448
+ });
3449
+ useEffect(() => {
3450
+ if (process.env.NODE_ENV === "production") return;
3451
+ const ids = new Set(resolved.commands.map((c) => c.id));
3452
+ for (const problem of validateChrome$1(resolved.chrome, ids)) console.warn(`[embedpdf] chrome: ${problem}`);
3453
+ for (const c of resolved.commands) if (c.icon && !(c.icon in ICON_PATHS) && !(c.icon in resolved.icons)) console.warn(`[embedpdf] command "${c.id}": unknown icon "${c.icon}"`);
3454
+ }, [resolved]);
3455
+ const [plugins] = useState(() => [
3456
+ stagePlugin({
3457
+ layout: "vertical",
3458
+ interaction: true
3459
+ }),
3460
+ stagePlugin({
3461
+ id: "stage-thumbs",
3462
+ token: ThumbsStageToken,
3463
+ layout: "grid",
3464
+ columns: 1,
3465
+ sizing: "uniform",
3466
+ zoom: { pageWidth: 150 },
3467
+ padding: 12,
3468
+ gap: { px: 16 },
3469
+ pageFrame: {
3470
+ top: 0,
3471
+ right: 0,
3472
+ bottom: 20,
3473
+ left: 0
3474
+ },
3475
+ fitAlign: {
3476
+ x: "center",
3477
+ y: "start"
3478
+ },
3479
+ scrollBehavior: "instant"
3480
+ }),
3481
+ renderPlugin(),
3482
+ pageEditPlugin(),
3483
+ interactionPlugin({ defaultTool: "pointer" }),
3484
+ selectionPlugin(),
3485
+ annotationPlugin({ tools: [{
3486
+ id: "arrow",
3487
+ extends: "line",
3488
+ defaults: { lineEndings: {
3489
+ start: "none",
3490
+ end: "open-arrow"
3491
+ } }
3492
+ }] }),
3493
+ formPlugin({ scripting: { enabled: true } }),
3494
+ linkPlugin(),
3495
+ redactionPlugin(),
3496
+ searchPlugin(),
3497
+ demoToolsPlugin(),
3498
+ i18nPlugin({
3499
+ locale: resolved.i18n.initial,
3500
+ fallbackLocale: "en",
3501
+ locales: resolved.i18n.locales,
3502
+ loaders: resolved.i18n.loaders
3503
+ }),
3504
+ commandsPlugin({
3505
+ commands: resolved.commands,
3506
+ disabledCategories: disabledCategories ? [...disabledCategories] : void 0
3507
+ }),
3508
+ shellPlugin()
3509
+ ]);
3510
+ const config = resolved;
3511
+ return /* @__PURE__ */ jsx(ThemeProvider, {
3512
+ preference: themeConfigOf(theme).preference,
3513
+ target: themeTarget,
3514
+ children: /* @__PURE__ */ jsx(Viewer, {
3515
+ engine,
3516
+ plugins,
3517
+ initialDocuments,
3518
+ fallback: fallback ?? /* @__PURE__ */ jsx(Booting, {}),
3519
+ children: /* @__PURE__ */ jsxs(ViewerConfigProvider, {
3520
+ value: config,
3521
+ children: [onViewer && /* @__PURE__ */ jsx(HandleBridge, { onViewer }), /* @__PURE__ */ jsx(Shell, {})]
3522
+ })
3523
+ })
3524
+ });
3525
+ }
3526
+ //#endregion
3527
+ export { AnnotationToken, CommandsToken, DocumentsToken, FormToken, FullViewer, I18nToken, InteractionToken, MetadataToken, RedactionToken, SearchToken, SelectionToken, ShellToken, StageToken, addItem, chromeHelpers, createViewerHandle, custom, defaultChrome, defaultCommands, ICON_PATHS as defaultIcons, defineChrome, group, item, removeItems, replaceItem, themeConfigOf, validateChrome };
3528
+
3529
+ //# sourceMappingURL=index.js.map