@embedpdf/viewer-chrome 3.0.0-next.13 → 3.0.0-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +175 -132
  2. package/dist/annotation-fonts-BqoLZ4Mp.js +80 -0
  3. package/dist/annotation-fonts-BqoLZ4Mp.js.map +1 -0
  4. package/dist/annotation-fonts-Dt0IQXhz.cjs +91 -0
  5. package/dist/annotation-fonts-Dt0IQXhz.cjs.map +1 -0
  6. package/dist/annotation-style--rPIZsuP.js +879 -0
  7. package/dist/annotation-style--rPIZsuP.js.map +1 -0
  8. package/dist/annotation-style-Bzvvp8-N.cjs +879 -0
  9. package/dist/annotation-style-Bzvvp8-N.cjs.map +1 -0
  10. package/dist/ar-D9XKAmDx.js +567 -0
  11. package/dist/ar-D9XKAmDx.js.map +1 -0
  12. package/dist/ar-DnEj5vbP.cjs +567 -0
  13. package/dist/ar-DnEj5vbP.cjs.map +1 -0
  14. package/dist/chrome.cjs +378 -221
  15. package/dist/chrome.cjs.map +1 -1
  16. package/dist/chrome.d.cts +5 -34
  17. package/dist/chrome.d.ts +5 -34
  18. package/dist/chrome.js +377 -218
  19. package/dist/chrome.js.map +1 -1
  20. package/dist/commands-BmgxhJ8B.js +953 -0
  21. package/dist/commands-BmgxhJ8B.js.map +1 -0
  22. package/dist/commands-CYt4SFUX.cjs +1060 -0
  23. package/dist/commands-CYt4SFUX.cjs.map +1 -0
  24. package/dist/commands.cjs +12 -595
  25. package/dist/commands.d.cts +29 -4
  26. package/dist/commands.d.ts +29 -4
  27. package/dist/commands.js +2 -593
  28. package/dist/comments-panel-MUVgLCBI.js +514 -0
  29. package/dist/comments-panel-MUVgLCBI.js.map +1 -0
  30. package/dist/comments-panel-pEcQg4Vs.cjs +514 -0
  31. package/dist/comments-panel-pEcQg4Vs.cjs.map +1 -0
  32. package/dist/de-Bdr4aWzm.js +566 -0
  33. package/dist/de-Bdr4aWzm.js.map +1 -0
  34. package/dist/de-Dv6zvbnd.cjs +566 -0
  35. package/dist/de-Dv6zvbnd.cjs.map +1 -0
  36. package/dist/dialog-B5GBisKv.js +55 -0
  37. package/dist/dialog-B5GBisKv.js.map +1 -0
  38. package/dist/dialog-t8F13BL_.cjs +60 -0
  39. package/dist/dialog-t8F13BL_.cjs.map +1 -0
  40. package/dist/es-BP09DR9w.cjs +566 -0
  41. package/dist/es-BP09DR9w.cjs.map +1 -0
  42. package/dist/es-BSXz5tBv.js +566 -0
  43. package/dist/es-BSXz5tBv.js.map +1 -0
  44. package/dist/form-design-panel-B5MXTZ96.cjs +418 -0
  45. package/dist/form-design-panel-B5MXTZ96.cjs.map +1 -0
  46. package/dist/form-design-panel-vWWU9uB-.js +418 -0
  47. package/dist/form-design-panel-vWWU9uB-.js.map +1 -0
  48. package/dist/fr-BBQQn7tJ.cjs +566 -0
  49. package/dist/fr-BBQQn7tJ.cjs.map +1 -0
  50. package/dist/fr-Ckn3K9v1.js +566 -0
  51. package/dist/fr-Ckn3K9v1.js.map +1 -0
  52. package/dist/he-DRsG7bH7.cjs +567 -0
  53. package/dist/he-DRsG7bH7.cjs.map +1 -0
  54. package/dist/he-wdGU5PkU.js +567 -0
  55. package/dist/he-wdGU5PkU.js.map +1 -0
  56. package/dist/icons-BrK5iVL8.js +856 -0
  57. package/dist/icons-BrK5iVL8.js.map +1 -0
  58. package/dist/icons-x0Tc05bs.cjs +1010 -0
  59. package/dist/icons-x0Tc05bs.cjs.map +1 -0
  60. package/dist/index.cjs +3162 -5106
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.d.cts +636 -143
  63. package/dist/index.d.ts +636 -143
  64. package/dist/index.js +3083 -4975
  65. package/dist/index.js.map +1 -1
  66. package/dist/it-BuP4o0xQ.cjs +566 -0
  67. package/dist/it-BuP4o0xQ.cjs.map +1 -0
  68. package/dist/it-Us-VmvIu.js +566 -0
  69. package/dist/it-Us-VmvIu.js.map +1 -0
  70. package/dist/ja-Cizj0-5O.js +566 -0
  71. package/dist/ja-Cizj0-5O.js.map +1 -0
  72. package/dist/ja-DovEd6ts.cjs +566 -0
  73. package/dist/ja-DovEd6ts.cjs.map +1 -0
  74. package/dist/ko-B-qJEol2.cjs +566 -0
  75. package/dist/ko-B-qJEol2.cjs.map +1 -0
  76. package/dist/ko-DlrXliE1.js +566 -0
  77. package/dist/ko-DlrXliE1.js.map +1 -0
  78. package/dist/labels-BiM9wp9K.cjs +21 -0
  79. package/dist/labels-BiM9wp9K.cjs.map +1 -0
  80. package/dist/labels-C6_i87Oj.js +16 -0
  81. package/dist/labels-C6_i87Oj.js.map +1 -0
  82. package/dist/link-editor-CSjqPnj1.cjs +125 -0
  83. package/dist/link-editor-CSjqPnj1.cjs.map +1 -0
  84. package/dist/link-editor-SxM1nnJU.js +125 -0
  85. package/dist/link-editor-SxM1nnJU.js.map +1 -0
  86. package/dist/measurement-Bs61XWbJ.js +325 -0
  87. package/dist/measurement-Bs61XWbJ.js.map +1 -0
  88. package/dist/measurement-CcCsHPOX.cjs +326 -0
  89. package/dist/measurement-CcCsHPOX.cjs.map +1 -0
  90. package/dist/nl-0aF7M_0h.js +566 -0
  91. package/dist/nl-0aF7M_0h.js.map +1 -0
  92. package/dist/nl-D3KZDDiJ.cjs +566 -0
  93. package/dist/nl-D3KZDDiJ.cjs.map +1 -0
  94. package/dist/pl-BW--AaCI.js +566 -0
  95. package/dist/pl-BW--AaCI.js.map +1 -0
  96. package/dist/pl-DSFhAUqx.cjs +566 -0
  97. package/dist/pl-DSFhAUqx.cjs.map +1 -0
  98. package/dist/pt-C5S-8MC5.cjs +566 -0
  99. package/dist/pt-C5S-8MC5.cjs.map +1 -0
  100. package/dist/pt-DIhpU5pT.js +566 -0
  101. package/dist/pt-DIhpU5pT.js.map +1 -0
  102. package/dist/redact-confirm-B6j36TPw.js +83 -0
  103. package/dist/redact-confirm-B6j36TPw.js.map +1 -0
  104. package/dist/redact-confirm-CX_SY2ZN.cjs +83 -0
  105. package/dist/redact-confirm-CX_SY2ZN.cjs.map +1 -0
  106. package/dist/redaction-panel-BmoIl-Ll.cjs +96 -0
  107. package/dist/redaction-panel-BmoIl-Ll.cjs.map +1 -0
  108. package/dist/redaction-panel-IJDshSJI.js +96 -0
  109. package/dist/redaction-panel-IJDshSJI.js.map +1 -0
  110. package/dist/search-panel-HfIhJm1M.js +249 -0
  111. package/dist/search-panel-HfIhJm1M.js.map +1 -0
  112. package/dist/search-panel-s9IoiOtv.cjs +249 -0
  113. package/dist/search-panel-s9IoiOtv.cjs.map +1 -0
  114. package/dist/sheet-BDsa_Sup.cjs +442 -0
  115. package/dist/sheet-BDsa_Sup.cjs.map +1 -0
  116. package/dist/sheet-R6XDgyEf.js +365 -0
  117. package/dist/sheet-R6XDgyEf.js.map +1 -0
  118. package/dist/sign-dialog-CgnR7akj.cjs +155 -0
  119. package/dist/sign-dialog-CgnR7akj.cjs.map +1 -0
  120. package/dist/sign-dialog-GDqCoQhA.js +155 -0
  121. package/dist/sign-dialog-GDqCoQhA.js.map +1 -0
  122. package/dist/signature-inspector-DyO313Qf.cjs +166 -0
  123. package/dist/signature-inspector-DyO313Qf.cjs.map +1 -0
  124. package/dist/signature-inspector-okFz8do6.js +166 -0
  125. package/dist/signature-inspector-okFz8do6.js.map +1 -0
  126. package/dist/signature-maker-BhCmYyHD.js +393 -0
  127. package/dist/signature-maker-BhCmYyHD.js.map +1 -0
  128. package/dist/signature-maker-BoYa8vgA.cjs +393 -0
  129. package/dist/signature-maker-BoYa8vgA.cjs.map +1 -0
  130. package/dist/signatures-panel-B56PC6d9.cjs +300 -0
  131. package/dist/signatures-panel-B56PC6d9.cjs.map +1 -0
  132. package/dist/signatures-panel-CgnFfhFm.js +300 -0
  133. package/dist/signatures-panel-CgnFfhFm.js.map +1 -0
  134. package/dist/stamp-store-BHvRs-Do.cjs +147 -0
  135. package/dist/stamp-store-BHvRs-Do.cjs.map +1 -0
  136. package/dist/stamp-store-WuF-L3Jj.js +112 -0
  137. package/dist/stamp-store-WuF-L3Jj.js.map +1 -0
  138. package/dist/stamps-panel-B9NYGayn.cjs +231 -0
  139. package/dist/stamps-panel-B9NYGayn.cjs.map +1 -0
  140. package/dist/stamps-panel-CDO0FPyK.js +231 -0
  141. package/dist/stamps-panel-CDO0FPyK.js.map +1 -0
  142. package/dist/toolbar-CJu2wKTU.cjs +1659 -0
  143. package/dist/toolbar-CJu2wKTU.cjs.map +1 -0
  144. package/dist/toolbar-z9od3tKl.js +1564 -0
  145. package/dist/toolbar-z9od3tKl.js.map +1 -0
  146. package/dist/zh-cn-3tmMeHqv.cjs +566 -0
  147. package/dist/zh-cn-3tmMeHqv.cjs.map +1 -0
  148. package/dist/zh-cn-CxkUUBct.js +566 -0
  149. package/dist/zh-cn-CxkUUBct.js.map +1 -0
  150. package/package.json +6 -4
  151. package/src/styles.css +153 -89
  152. package/dist/commands.cjs.map +0 -1
  153. package/dist/commands.js.map +0 -1
  154. package/dist/es-DYwzfxyt.cjs +0 -293
  155. package/dist/es-DYwzfxyt.cjs.map +0 -1
  156. package/dist/es-sN6uqBgD.js +0 -293
  157. package/dist/es-sN6uqBgD.js.map +0 -1
@@ -0,0 +1,1659 @@
1
+ const require_icons = require("./icons-x0Tc05bs.cjs");
2
+ const require_sheet = require("./sheet-BDsa_Sup.cjs");
3
+ const require_stamp_store = require("./stamp-store-BHvRs-Do.cjs");
4
+ let react = require("react");
5
+ let _embedpdf_react_runtime = require("@embedpdf/react/runtime");
6
+ let _embedpdf_react_stage = require("@embedpdf/react/stage");
7
+ let _embedpdf_react_page_edit = require("@embedpdf/react/page-edit");
8
+ let _embedpdf_react_interaction = require("@embedpdf/react/interaction");
9
+ let _embedpdf_react_annotation = require("@embedpdf/react/annotation");
10
+ let _embedpdf_react_stamp = require("@embedpdf/react/stamp");
11
+ let _embedpdf_react_measurement = require("@embedpdf/react/measurement");
12
+ let _embedpdf_react_redaction = require("@embedpdf/react/redaction");
13
+ let _embedpdf_react_form = require("@embedpdf/react/form");
14
+ let _embedpdf_react_signature = require("@embedpdf/react/signature");
15
+ let _embedpdf_react_i18n = require("@embedpdf/react/i18n");
16
+ let _embedpdf_react_commands = require("@embedpdf/react/commands");
17
+ let _embedpdf_react_shell = require("@embedpdf/react/shell");
18
+ let _embedpdf_core_ui = require("@embedpdf/core-ui");
19
+ let _embedpdf_web = require("@embedpdf/web");
20
+ let react_jsx_runtime = require("react/jsx-runtime");
21
+ let _embedpdf_react_toolbar = require("@embedpdf/react/toolbar");
22
+ //#region src/features.ts
23
+ /** Every built-in feature and its parts: the names `disable` takes, as All features lists them. */
24
+ const FEATURE_PARTS = {
25
+ annotations: [
26
+ "highlight",
27
+ "markup",
28
+ "drawing",
29
+ "text",
30
+ "notes",
31
+ "links"
32
+ ],
33
+ shapes: [],
34
+ stamps: [],
35
+ comments: ["writing"],
36
+ forms: ["filling", "building"],
37
+ signatures: ["signing", "checking"],
38
+ redaction: [],
39
+ measurement: [],
40
+ pages: [],
41
+ attachments: ["adding"],
42
+ text: ["selection", "copy"],
43
+ search: [],
44
+ thumbnails: [],
45
+ outline: [],
46
+ documents: [
47
+ "open",
48
+ "download",
49
+ "print",
50
+ "tabs"
51
+ ]
52
+ };
53
+ /** The features that change the document: what `readOnly` turns off. */
54
+ const READ_ONLY_FEATURES = [
55
+ "annotations",
56
+ "shapes",
57
+ "stamps",
58
+ "comments.writing",
59
+ "forms",
60
+ "signatures.signing",
61
+ "redaction",
62
+ "measurement",
63
+ "pages",
64
+ "attachments.adding"
65
+ ];
66
+ const isInside = (feature, outer) => feature === outer || feature.startsWith(`${outer}.`);
67
+ /**
68
+ * Whether nothing that changes the document is left, `isOff` saying which features are off (the
69
+ * commands plugin's `isCategoryDisabled`): what the View only label shows for.
70
+ */
71
+ const leavesNothingToChange = (isOff) => READ_ONLY_FEATURES.every(isOff);
72
+ /**
73
+ * The annotations each feature makes, by the names the annotation plugin's `editable` setting
74
+ * takes: a subtype, or a measurement's intent. Every subtype people can change is here; a popup
75
+ * or an unsupported annotation is never changed by hand. A signature placed off a field is a
76
+ * stamp; one drawn into a field is the field's value.
77
+ */
78
+ const FEATURE_ANNOTATIONS = {
79
+ "annotations.highlight": ["highlight"],
80
+ "annotations.markup": [
81
+ "underline",
82
+ "strikeout",
83
+ "squiggly"
84
+ ],
85
+ "annotations.drawing": ["ink"],
86
+ "annotations.text": ["free-text", "caret"],
87
+ "annotations.notes": ["text"],
88
+ "annotations.links": ["link"],
89
+ shapes: [
90
+ "square",
91
+ "circle",
92
+ "line",
93
+ "polygon",
94
+ "polyline"
95
+ ],
96
+ stamps: ["stamp"],
97
+ redaction: ["redact"],
98
+ measurement: [
99
+ "line-dimension",
100
+ "polyline-dimension",
101
+ "polygon-dimension"
102
+ ],
103
+ "attachments.adding": ["file-attachment"],
104
+ "forms.building": ["widget"]
105
+ };
106
+ /** The fields each feature fills in, by the families the form plugin's `fillable` setting takes. */
107
+ const FEATURE_FIELDS = {
108
+ "forms.filling": [
109
+ "text",
110
+ "checkbox",
111
+ "radio",
112
+ "combobox",
113
+ "listbox",
114
+ "pushbutton"
115
+ ],
116
+ "signatures.signing": ["signature"]
117
+ };
118
+ /** `true` while no feature of `table` is off, `false` once every one is, else what the ones on hold. */
119
+ function stillOn(table, off) {
120
+ const features = Object.keys(table);
121
+ const on = features.filter((feature) => !off.some((name) => isInside(feature, name)));
122
+ if (on.length === features.length) return true;
123
+ if (on.length === 0) return false;
124
+ return on.flatMap((feature) => table[feature]);
125
+ }
126
+ const partsOf = (feature) => FEATURE_PARTS[feature] ?? [];
127
+ /**
128
+ * The features `readOnly` turns off, minus `except`. An exception inside a feature splits it
129
+ * into its parts, so `except: ['forms.filling']` leaves `forms.building` off.
130
+ */
131
+ function readOnlyFeaturesOf(readOnly) {
132
+ if (!readOnly) return [];
133
+ const except = readOnly === true ? [] : readOnly.except;
134
+ const off = [];
135
+ const subtract = (feature) => {
136
+ if (except.some((kept) => isInside(feature, kept))) return;
137
+ const parts = partsOf(feature);
138
+ if (parts.length === 0 || !except.some((kept) => isInside(kept, feature))) {
139
+ off.push(feature);
140
+ return;
141
+ }
142
+ for (const part of parts) subtract(`${feature}.${part}`);
143
+ };
144
+ for (const feature of READ_ONLY_FEATURES) subtract(feature);
145
+ return off;
146
+ }
147
+ /** The features `disable` and `readOnly` turn off: the names without a colon, and `readOnly`'s. */
148
+ function featuresOffOf(disable, readOnly) {
149
+ const features = (disable ?? []).filter((name) => !name.includes(":"));
150
+ return [.../* @__PURE__ */ new Set([...features, ...readOnlyFeaturesOf(readOnly)])];
151
+ }
152
+ /**
153
+ * The commands plugin's settings for a `disable` list and a `readOnly` value: the names with a
154
+ * colon are command ids, the rest features, and `readOnly` adds its own features.
155
+ */
156
+ function disabledSettingsOf(disable, readOnly) {
157
+ const commands = new Set((disable ?? []).filter((name) => name.includes(":")));
158
+ return {
159
+ disabledCategories: featuresOffOf(disable, readOnly),
160
+ disabledCommands: [...commands]
161
+ };
162
+ }
163
+ /**
164
+ * What people may still change by hand for a `disable` list and a `readOnly` value: the
165
+ * annotations and fields of the features left on. A single command turned off leaves them.
166
+ */
167
+ function byHandSettingsOf(disable, readOnly) {
168
+ const off = featuresOffOf(disable, readOnly);
169
+ return {
170
+ editable: stillOn(FEATURE_ANNOTATIONS, off),
171
+ fillable: stillOn(FEATURE_FIELDS, off)
172
+ };
173
+ }
174
+ /**
175
+ * What two `editable` or `fillable` values both allow: `true` is everything, `false` nothing,
176
+ * and a list its names, in the order of `left`. The viewer's own value and the app's setting
177
+ * meet here, so each can only take away.
178
+ */
179
+ function bothAllow(left, right) {
180
+ if (left === true) return right;
181
+ if (right === true) return left;
182
+ if (left === false || right === false) return false;
183
+ return left.filter((name) => right.includes(name));
184
+ }
185
+ /** Whether two `editable` or `fillable` values say the same. */
186
+ const sameByHand = (left, right) => typeof left === "boolean" || typeof right === "boolean" ? left === right : left.length === right.length && left.every((name, index) => name === right[index]);
187
+ //#endregion
188
+ //#region src/ui/bar-edge.tsx
189
+ /**
190
+ * Where a bar is, for what opens from it. The shell wraps each bar it draws in
191
+ * a `BarEdgeProvider` naming the viewer edge the bar sits on; a menu (and the
192
+ * toolbar's own "More") reads it with `useBarEdge()` and opens away from that
193
+ * edge: a bottom bar's menus open upward, a start bar's to the side, a
194
+ * floating bar's toward the middle of the viewer.
195
+ *
196
+ * A floating bar names the half of the viewer it floats in (`float-top`,
197
+ * `float-bottom`), since its menus open toward the other half; a bar over a
198
+ * selection says `selection`, and its menus open on the side the bar sits,
199
+ * above or below what's selected, away from it. Outside any provider the edge
200
+ * is `'top'`, the main bar's.
201
+ */
202
+ const BarEdgeContext = (0, react.createContext)("top");
203
+ /** Names the edge of the bar drawn inside it. */
204
+ const BarEdgeProvider = BarEdgeContext.Provider;
205
+ /** The edge of the bar this component is drawn in (`'top'` outside any bar). */
206
+ function useBarEdge() {
207
+ return (0, react.useContext)(BarEdgeContext);
208
+ }
209
+ //#endregion
210
+ //#region src/ui/measurement-scale.tsx
211
+ /**
212
+ * The Measure bar's scale button: the current page's drawing scale, which opens the measurement
213
+ * panel. Apart from the panel and the calibration dialog (measurement.tsx), so the bar draws it
214
+ * without loading them.
215
+ */
216
+ const button = "border-border text-fg hover:bg-hover rounded-md border px-3 py-1.5 text-sm disabled:opacity-50";
217
+ function MeasurementScaleButton() {
218
+ const t = (0, _embedpdf_react_i18n.useT)();
219
+ const scale = (0, _embedpdf_react_measurement.usePageScale)((0, _embedpdf_react_runtime.useSelector)(_embedpdf_react_stage.StageToken, (stage) => stage.getCurrentPage()));
220
+ const surface = (0, _embedpdf_react_shell.useSurface)("measurement");
221
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
222
+ part: "toolbar-button",
223
+ className: `${button} inline-flex items-center gap-1.5 whitespace-nowrap`,
224
+ onClick: () => surface.open(),
225
+ title: t("measurement.title"),
226
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
227
+ name: "updateScale",
228
+ size: 20,
229
+ className: "shrink-0"
230
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
231
+ t("measurement.scale"),
232
+ ":",
233
+ " ",
234
+ scale?.measure?.subtype === "rectilinear" ? scale.measure.ratio ?? t("measurement.custom") : t("measurement.unavailable")
235
+ ] })]
236
+ });
237
+ }
238
+ function PageNumber() {
239
+ const t = (0, _embedpdf_react_i18n.useT)();
240
+ const stage = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_stage.StageToken);
241
+ const current = (0, _embedpdf_react_stage.useStageState)((state) => state.currentPageIndex + 1);
242
+ const total = (0, _embedpdf_react_stage.useStageState)((state) => state.pageCount);
243
+ /** What the field holds while it is open, or `null` while the number shows. */
244
+ const [draft, setDraft] = (0, react.useState)(null);
245
+ const field = (0, react.useRef)(null);
246
+ const button = (0, react.useRef)(null);
247
+ /** Whether the field closed from the keyboard, so focus goes back to the number. */
248
+ const refocus = (0, react.useRef)(false);
249
+ const editing = draft !== null;
250
+ (0, react.useEffect)(() => {
251
+ if (editing) {
252
+ field.current?.focus();
253
+ field.current?.select();
254
+ } else if (refocus.current) {
255
+ refocus.current = false;
256
+ button.current?.focus();
257
+ }
258
+ }, [editing]);
259
+ if (!total) return null;
260
+ /** Closes the field from the keyboard: focus goes back to the number. */
261
+ const close = () => {
262
+ refocus.current = true;
263
+ setDraft(null);
264
+ };
265
+ const go = () => {
266
+ const page = Number.parseInt(draft ?? "", 10);
267
+ if (Number.isFinite(page)) stage?.goToPage(Math.min(Math.max(page, 1), total) - 1);
268
+ close();
269
+ };
270
+ const [before, after = ""] = t("demo.page", { params: {
271
+ current: "",
272
+ total
273
+ } }).split("");
274
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
275
+ className: "text-fg-secondary block whitespace-nowrap px-3.5 text-sm leading-7",
276
+ children: [
277
+ before,
278
+ editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
279
+ ref: field,
280
+ type: "text",
281
+ inputMode: "numeric",
282
+ "aria-label": t("demo.goToPage"),
283
+ value: draft,
284
+ onChange: (event) => setDraft(event.target.value.replace(/[^0-9]/g, "")),
285
+ onKeyDown: (event) => {
286
+ if (event.key === "Enter") {
287
+ event.preventDefault();
288
+ go();
289
+ } else if (event.key === "Escape") {
290
+ event.preventDefault();
291
+ close();
292
+ }
293
+ },
294
+ onBlur: () => setDraft(null),
295
+ style: { width: `${String(total).length + 1}ch` },
296
+ className: "bg-hover ring-accent text-fg rounded text-center tabular-nums outline-none ring-1"
297
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
298
+ ref: button,
299
+ type: "button",
300
+ title: t("demo.goToPage"),
301
+ onClick: () => setDraft(String(current)),
302
+ className: "hover:bg-hover hover:text-fg -mx-0.5 cursor-pointer rounded px-0.5 tabular-nums",
303
+ children: current
304
+ }),
305
+ after
306
+ ]
307
+ });
308
+ }
309
+ //#endregion
310
+ //#region src/ui/quick-stamps.tsx
311
+ /**
312
+ * Quick marks in the toolbar: a row of stamp thumbnails that arm on click —
313
+ * the checkmark, the cross, whatever the embedder configured
314
+ * (`plugins.stamp.quick`: `library:name`, `standard` for the standard stamps). The hover ghost and
315
+ * the click placement are the annotation plugin's, exactly as from the
316
+ * sidebar; only the picker moved. Renders nothing until the assets exist.
317
+ * Lives in the workspace toolbar, so it reads the active document's tool.
318
+ */
319
+ function QuickStamps() {
320
+ const t = (0, _embedpdf_react_i18n.useT)();
321
+ const stamp = (0, _embedpdf_react_stamp.useStamp)();
322
+ const { standard, quick } = require_icons.useStampSettings();
323
+ const { locale } = (0, _embedpdf_react_i18n.useI18nState)();
324
+ const assets = (0, _embedpdf_react_stamp.useStampAssets)();
325
+ const documentId = (0, _embedpdf_react_runtime.useDocumentId)();
326
+ const activeToolId = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_interaction.InteractionToken, (interaction) => interaction.getActiveToolId(), null);
327
+ const armedId = (0, _embedpdf_react_stamp.useStampState)((state) => state.armedAsset?.id ?? null);
328
+ const ids = (quick ?? []).map(require_stamp_store.quickStampAssetIdOf);
329
+ const wantsDefault = ids.some((id) => id.startsWith(`${require_stamp_store.DEFAULT_LIBRARY_ID}:`));
330
+ (0, react.useEffect)(() => {
331
+ if (!wantsDefault) return;
332
+ require_stamp_store.restoreStampLibrariesOnce(stamp).then(() => require_stamp_store.ensureDefaultLibrary(stamp, require_stamp_store.resolveStampsLocale(locale), standard)).catch((error) => console.error("[embedpdf] quick stamps failed:", error));
333
+ }, [
334
+ stamp,
335
+ locale,
336
+ wantsDefault
337
+ ]);
338
+ const shown = ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset);
339
+ if (shown.length === 0 || !documentId) return null;
340
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
341
+ className: "flex items-center gap-1",
342
+ role: "group",
343
+ "aria-label": t("demo.quickStampsLabel"),
344
+ children: shown.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStampButton, {
345
+ asset,
346
+ active: activeToolId === "stamp" && armedId === asset.id,
347
+ onArm: () => void stamp.armAsset(asset.id).catch((error) => console.error("[embedpdf] arm failed", error))
348
+ }, asset.id))
349
+ });
350
+ }
351
+ function QuickStampButton({ asset, active, onArm }) {
352
+ const url = (0, _embedpdf_react_stamp.useStampAssetPreviewUrl)(asset.id);
353
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
354
+ type: "button",
355
+ onClick: onArm,
356
+ title: asset.label,
357
+ "aria-label": asset.label,
358
+ "aria-pressed": active || void 0,
359
+ part: buttonPart(active),
360
+ className: buttonClass(active, true),
361
+ children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
362
+ src: url,
363
+ alt: "",
364
+ className: "h-5 w-5 object-contain",
365
+ draggable: false
366
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
367
+ className: "text-xs",
368
+ children: asset.label
369
+ })
370
+ });
371
+ }
372
+ //#endregion
373
+ //#region src/ui/view-only.tsx
374
+ /**
375
+ * The View only label: the `{ custom: 'view-only' }` item, in the main bar by default. It says why
376
+ * the tools are gone, while nothing that changes the document is on (`readOnly`, or `disable`
377
+ * naming every such feature), or while the scope lets this user change nothing in the open
378
+ * document. Otherwise it draws nothing.
379
+ */
380
+ /** Whether the scope lets this user change an annotation: make one, or change or delete one there. */
381
+ function mayChangeAnnotations(annotation) {
382
+ if (annotation.canCreate()) return true;
383
+ return annotation.list().some(({ ref }) => annotation.canUpdate(ref) || annotation.canDelete(ref));
384
+ }
385
+ /** Whether the scope lets this user change the form: its fields, a value, or a signature. */
386
+ function mayChangeForm(form, signature) {
387
+ if (form.canDesign()) return true;
388
+ return form.list().some((field) => field.family === "signature" ? signature?.canSign(field.ref) ?? false : form.canFill(field.ref));
389
+ }
390
+ /**
391
+ * Whether the scope lets this user change nothing in the open document: no annotation to make,
392
+ * change or delete (comments included), no field to fill in, sign or design, no page to edit and
393
+ * no redaction to apply. Asked of the plugins' `can*` twins; `false` while no document is open.
394
+ */
395
+ function useNothingAllowed() {
396
+ const open = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_annotation.AnnotationToken) !== null;
397
+ const signature = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_signature.SignatureToken);
398
+ const annotates = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_annotation.AnnotationToken, mayChangeAnnotations, false);
399
+ const fills = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_form.FormToken, (form) => mayChangeForm(form, signature), false);
400
+ const editsPages = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_page_edit.PageEditToken, (pageEdit) => pageEdit.canEdit(), false);
401
+ const redacts = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_redaction.RedactionToken, (redaction) => redaction.canApply(), false);
402
+ return open && !annotates && !fills && !editsPages && !redacts;
403
+ }
404
+ /**
405
+ * The label; `compact` draws its icon alone (a narrow bar, or a vertical one), its words still
406
+ * there for a screen reader. Plain text, not a live region: it says what the viewer is, and the
407
+ * refusal notice stays the viewer's one status message. Its tooltip says why.
408
+ */
409
+ function ViewOnlyLabel({ compact = false }) {
410
+ const t = (0, _embedpdf_react_i18n.useT)();
411
+ const commands = (0, _embedpdf_react_commands.useCommands)();
412
+ (0, _embedpdf_react_commands.useCommandsSettings)((settings) => settings.disabledCategories);
413
+ const turnedOff = leavesNothingToChange((feature) => commands.isCategoryDisabled(feature));
414
+ const nothingAllowed = useNothingAllowed();
415
+ if (!turnedOff && !nothingAllowed) return null;
416
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
417
+ title: t(turnedOff ? "viewOnly.turnedOff" : "viewOnly.notAllowed"),
418
+ className: "bg-hover text-fg-secondary flex h-7 shrink-0 select-none items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-xs font-medium",
419
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
420
+ name: "eye",
421
+ size: 16,
422
+ className: "shrink-0"
423
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
424
+ className: compact ? "sr-only" : void 0,
425
+ children: t("viewOnly.label")
426
+ })]
427
+ });
428
+ }
429
+ //#endregion
430
+ //#region src/ui/item-context.ts
431
+ /** The attribute each field of a {@link ToolbarItemContext} is written to. */
432
+ const TOOLBAR_ITEM_CONTEXT_ATTRIBUTES = {
433
+ orientation: "data-orientation",
434
+ edge: "data-edge",
435
+ variant: "data-variant",
436
+ size: "data-size"
437
+ };
438
+ /** The public place of a bar on `edge`: a floating bar is `float`, whichever half it floats in. */
439
+ const toolbarItemEdgeOf = (edge) => edge === "float-top" || edge === "float-bottom" ? "float" : edge;
440
+ /** Write `context` on `element`, changing only the attributes that differ. */
441
+ function writeToolbarItemContext(element, context) {
442
+ for (const [field, attribute] of Object.entries(TOOLBAR_ITEM_CONTEXT_ATTRIBUTES)) {
443
+ const value = context[field];
444
+ if (value === null) element.removeAttribute(attribute);
445
+ else if (element.getAttribute(attribute) !== value) element.setAttribute(attribute, value);
446
+ }
447
+ }
448
+ //#endregion
449
+ //#region src/ui/slotted-edges.ts
450
+ /**
451
+ * The edge of the bar each slotted toolbar item is in, by the element slotted
452
+ * there, for the viewer's own components inside it: they're drawn apart from
453
+ * the bar (`placed-components.tsx`), and a menu among them opens away from
454
+ * the bar's edge like the bar's own. The item sockets write it
455
+ * (`item-socket.tsx`); outside any item, a component opens its menus like the
456
+ * main bar's.
457
+ */
458
+ /** The element `node` is a child of: its parent, or the host of the shadow root it is in. */
459
+ const parentOf = (node) => node.parentElement ?? node.getRootNode().host ?? null;
460
+ function createSlottedEdges() {
461
+ const edges = /* @__PURE__ */ new WeakMap();
462
+ const listeners = /* @__PURE__ */ new Set();
463
+ let version = 0;
464
+ return {
465
+ set(element, edge) {
466
+ if (edges.get(element) === edge) return;
467
+ edges.set(element, edge);
468
+ version += 1;
469
+ for (const listener of [...listeners]) listener();
470
+ },
471
+ edgeOf(element) {
472
+ for (let node = element; node; node = parentOf(node)) {
473
+ const edge = edges.get(node);
474
+ if (edge) return edge;
475
+ }
476
+ return "top";
477
+ },
478
+ subscribe(listener) {
479
+ listeners.add(listener);
480
+ return () => {
481
+ listeners.delete(listener);
482
+ };
483
+ },
484
+ version: () => version
485
+ };
486
+ }
487
+ const SlottedEdgesContext = (0, react.createContext)(createSlottedEdges());
488
+ /** Gives the item sockets and the viewer's own components inside it one {@link SlottedEdges}. */
489
+ const SlottedEdgesProvider = SlottedEdgesContext.Provider;
490
+ function useSlottedEdges() {
491
+ return (0, react.useContext)(SlottedEdgesContext);
492
+ }
493
+ //#endregion
494
+ //#region src/ui/item-socket.tsx
495
+ /**
496
+ * Your own toolbar item, in its bar: a `<slot name="item:<id>">` the browser
497
+ * shows your element in, with what the item stands for when nothing is
498
+ * slotted (its command's button). What's slotted lives in the page, so the
499
+ * socket is measured where it renders; the toolbar's measurement layer can't
500
+ * hold it. It tells what's slotted where it sits (`item-context.ts`), and the
501
+ * viewer's own components inside it learn the bar's edge from it, so their
502
+ * menus open away from the bar.
503
+ */
504
+ /** The socket's box: a row of what's slotted, at its own size along the bar. */
505
+ const SOCKET_STYLE = {
506
+ display: "inline-flex",
507
+ alignItems: "center",
508
+ flexShrink: 0
509
+ };
510
+ function ItemSocket({ id, variant, orientation, measure, children }) {
511
+ const slot = (0, react.useRef)(null);
512
+ const edge = useBarEdge();
513
+ const { size } = require_icons.useLayoutEnv();
514
+ const edges = useSlottedEdges();
515
+ const report = (0, react.useRef)(measure);
516
+ report.current = measure;
517
+ const resizes = (0, react.useRef)(null);
518
+ const hasVariants = variant !== null;
519
+ (0, react.useLayoutEffect)(() => {
520
+ const element = slot.current;
521
+ if (!element) return;
522
+ const side = orientation === "vertical" ? "height" : "width";
523
+ const measureNow = () => report.current(element.getBoundingClientRect()[side]);
524
+ if (!hasVariants) measureNow();
525
+ const observer = new ResizeObserver(measureNow);
526
+ observer.observe(element);
527
+ resizes.current = observer;
528
+ return () => {
529
+ observer.disconnect();
530
+ resizes.current = null;
531
+ };
532
+ }, [orientation, hasVariants]);
533
+ (0, react.useLayoutEffect)(() => {
534
+ const element = slot.current;
535
+ const observer = resizes.current;
536
+ if (!element || !observer) return;
537
+ observer.unobserve(element);
538
+ observer.observe(element);
539
+ }, [variant]);
540
+ (0, react.useLayoutEffect)(() => {
541
+ const element = slot.current;
542
+ if (!element) return;
543
+ const context = {
544
+ orientation,
545
+ edge: toolbarItemEdgeOf(edge),
546
+ variant,
547
+ size
548
+ };
549
+ const tell = () => {
550
+ for (const slotted of element.assignedElements()) {
551
+ writeToolbarItemContext(slotted, context);
552
+ edges.set(slotted, edge);
553
+ }
554
+ };
555
+ tell();
556
+ element.addEventListener("slotchange", tell);
557
+ return () => element.removeEventListener("slotchange", tell);
558
+ }, [
559
+ orientation,
560
+ edge,
561
+ variant,
562
+ size,
563
+ edges
564
+ ]);
565
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("slot", {
566
+ ref: slot,
567
+ name: `item:${id}`,
568
+ style: SOCKET_STYLE,
569
+ children
570
+ });
571
+ }
572
+ //#endregion
573
+ //#region src/ui/menu-placement.ts
574
+ /**
575
+ * The placement of a menu opened from a button in a bar on `edge`: below a
576
+ * top bar's button, above a bottom bar's, beside a start or end bar's (on the
577
+ * side of the pages), toward the middle from a floating bar, and from a bar
578
+ * over a selection on the side the bar sits (`selectionBar`), away from
579
+ * what's selected.
580
+ */
581
+ function menuPlacementOf(edge, direction, align = "start", selectionBar = "above") {
582
+ if (edge === "start" || edge === "end") return `${edge === "start" === (direction === "ltr") ? "right" : "left"}-${align}`;
583
+ return `${edge === "bottom" || edge === "float-bottom" || edge === "selection" && selectionBar === "above" ? "top" : "bottom"}-${align === "start" === (direction === "ltr") ? "start" : "end"}`;
584
+ }
585
+ /** The placement of a submenu: beside its row, on the side the text runs toward, tops lined up. */
586
+ function submenuPlacementOf(direction) {
587
+ return direction === "ltr" ? "right-start" : "left-start";
588
+ }
589
+ /** The key that opens a submenu from its row, and the one that closes it again. */
590
+ function submenuKeysOf(direction) {
591
+ return direction === "ltr" ? {
592
+ open: "ArrowRight",
593
+ close: "ArrowLeft"
594
+ } : {
595
+ open: "ArrowLeft",
596
+ close: "ArrowRight"
597
+ };
598
+ }
599
+ //#endregion
600
+ //#region src/ui/menu.tsx
601
+ /**
602
+ * The chrome's menus: a layout menu (sections of command ids) and the menus
603
+ * the toolbar derives (the "More" menu, a folded group's), drawn as rows in a
604
+ * popover that opens away from its bar's edge (`menu-placement.ts`). The
605
+ * popover is in the browser's top layer, so no bar, panel or the viewer's own
606
+ * box clips it, and it takes the menu keys from `@embedpdf/web`. Rows resolve
607
+ * live through the command registry: label, icon, shortcut, whether it's the
608
+ * current choice, enabled state; a row whose command isn't visible is gone, and a section
609
+ * with none left goes with its line. A row whose command opens a menu opens
610
+ * it beside it. On a phone (a compact viewer) every menu is a sheet from the
611
+ * bottom of the viewer instead, and a submenu slides in over the menu it
612
+ * opened from, with a row that goes back.
613
+ */
614
+ /** Px between a menu and its button, and between a menu and the window's edges. */
615
+ const MENU_GAP_PX = 4;
616
+ /** The `popover` attribute, which React's types don't list yet. */
617
+ const POPOVER = { popover: "manual" };
618
+ /** The rows of a menu, the elements the menu keys move between. */
619
+ const MENU_ITEM_SELECTOR = "[role=\"menuitem\"], [role=\"menuitemradio\"], [role=\"menuitemcheckbox\"]";
620
+ /** A copy no one can use: the toolbar's hidden measurement layer draws every button twice. */
621
+ const HIDDEN_COPY_SELECTOR = "[inert], [aria-hidden=\"true\"]";
622
+ /** Inside a menu, a menu is a submenu: it opens beside its row. */
623
+ const InMenuContext = (0, react.createContext)(false);
624
+ /** The rows of `menu` itself, not those of a submenu open inside it. */
625
+ const itemsOf = (menu) => [...menu.querySelectorAll(MENU_ITEM_SELECTOR)].filter((item) => item.closest("[role=\"menu\"]") === menu);
626
+ /** The button that opens the menu: the one beside it that says it has a popup. */
627
+ const triggerOf = (holder, menu) => [...holder.querySelectorAll("[aria-haspopup]")].find((candidate) => !menu.contains(candidate)) ?? null;
628
+ /**
629
+ * Where the bar over a selection that `element` is in sits now: its anchored wrapper says so
630
+ * in `data-placement` (`Anchored` in `@embedpdf/react`), `bottom…` once it went below.
631
+ */
632
+ const selectionBarSideOf = (element) => element.closest("[data-placement]")?.getAttribute("data-placement")?.startsWith("bottom") ? "below" : "above";
633
+ const boxOf = (element) => {
634
+ const rect = element.getBoundingClientRect();
635
+ return {
636
+ x: rect.left,
637
+ y: rect.top,
638
+ width: rect.width,
639
+ height: rect.height
640
+ };
641
+ };
642
+ /**
643
+ * What a submenu opens beside: its row, across the whole width of the menu
644
+ * the row is in, and raised by the submenu's own padding so its first row
645
+ * lines up with the row that opened it.
646
+ */
647
+ function submenuBoxOf(holder, submenu) {
648
+ const row = boxOf(holder);
649
+ const parent = holder.closest("[role=\"menu\"]");
650
+ const across = parent ? boxOf(parent) : row;
651
+ const inset = submenu.clientTop + parseFloat(getComputedStyle(submenu).paddingTop || "0");
652
+ return {
653
+ x: across.x,
654
+ y: row.y - inset,
655
+ width: across.width,
656
+ height: row.height
657
+ };
658
+ }
659
+ /**
660
+ * Place `menu` around `box` in the window: on the side `placement` asks for,
661
+ * or the other side when that has no room, moved to stay on screen, and never
662
+ * taller than the window.
663
+ */
664
+ function placeMenu(menu, box, placement, gap) {
665
+ const view = (0, _embedpdf_web.anchoredViewOf)(menu, "client");
666
+ if (!view) return;
667
+ menu.style.maxHeight = `${Math.max(0, view.height - 2 * MENU_GAP_PX)}px`;
668
+ const { left, top } = (0, _embedpdf_web.fitAnchoredRect)(box, placement, gap, {
669
+ size: {
670
+ width: menu.offsetWidth,
671
+ height: menu.offsetHeight
672
+ },
673
+ view
674
+ });
675
+ menu.style.left = `${left}px`;
676
+ menu.style.top = `${top}px`;
677
+ const landed = menu.getBoundingClientRect();
678
+ if (Math.abs(landed.left - left) > .5 || Math.abs(landed.top - top) > .5) {
679
+ menu.style.left = `${2 * left - landed.left}px`;
680
+ menu.style.top = `${2 * top - landed.top}px`;
681
+ }
682
+ }
683
+ /**
684
+ * Open `menu` from the button beside it in `holder`: show it in the top layer,
685
+ * place it and keep it placed, close it on a press outside it and its button,
686
+ * and drive it from the keyboard (a submenu's row also opens it with the arrow
687
+ * toward it, and the arrow back closes it). Returns the close.
688
+ */
689
+ function openMenu(menu, holder, options) {
690
+ const { nested, close } = options;
691
+ const trigger = triggerOf(holder, menu);
692
+ const direction = (0, _embedpdf_web.directionOf)(holder);
693
+ const placement = options.placement(direction);
694
+ const name = trigger?.getAttribute("aria-label") || trigger?.getAttribute("title") || trigger?.textContent?.trim();
695
+ if (name) menu.setAttribute("aria-label", name);
696
+ if (typeof menu.showPopover === "function") menu.showPopover();
697
+ const place = () => placeMenu(menu, nested ? submenuBoxOf(holder, menu) : boxOf(holder), placement, nested ? 0 : MENU_GAP_PX);
698
+ place();
699
+ const resizes = new ResizeObserver(place);
700
+ resizes.observe(menu);
701
+ resizes.observe(holder);
702
+ const stopGeometry = (0, _embedpdf_web.observeClientGeometry)(place);
703
+ const stopOutside = (0, _embedpdf_web.observeOutsidePress)(menu, (event) => {
704
+ if (!trigger || !event.composedPath().includes(trigger)) close();
705
+ });
706
+ const detachKeys = (0, _embedpdf_web.attachMenuKeys)(menu, {
707
+ trigger,
708
+ close,
709
+ items: () => itemsOf(menu)
710
+ });
711
+ const keys = submenuKeysOf(direction);
712
+ const onKeyDown = (event) => {
713
+ const item = event.target;
714
+ const free = !event.defaultPrevented;
715
+ const opensMenu = item.getAttribute("aria-haspopup") === "menu";
716
+ if (free && event.key === keys.open && opensMenu && itemsOf(menu).includes(item)) {
717
+ event.preventDefault();
718
+ if (item.getAttribute("aria-expanded") !== "true") item.click();
719
+ else {
720
+ const submenu = item.parentElement?.querySelector("[role=\"menu\"]");
721
+ if (submenu) itemsOf(submenu)[0]?.focus();
722
+ }
723
+ } else if (free && event.key === keys.close && nested) {
724
+ event.preventDefault();
725
+ close();
726
+ trigger?.focus();
727
+ }
728
+ if (nested && event.key !== "Tab") event.stopPropagation();
729
+ };
730
+ menu.addEventListener("keydown", onKeyDown);
731
+ return () => {
732
+ menu.removeEventListener("keydown", onKeyDown);
733
+ detachKeys();
734
+ stopOutside();
735
+ stopGeometry();
736
+ resizes.disconnect();
737
+ if (typeof menu.hidePopover === "function" && menu.matches(":popover-open")) menu.hidePopover();
738
+ };
739
+ }
740
+ /**
741
+ * A menu's surface, opened by the button beside it in the same parent (the
742
+ * one with `aria-haspopup`). It opens away from its bar's edge, or beside its
743
+ * row in another menu, and closes on Escape, Tab or a press anywhere outside
744
+ * it and its button. In a compact viewer it opens as a sheet.
745
+ */
746
+ function MenuPopover(props) {
747
+ const probe = (0, react.useRef)(null);
748
+ const [usable, setUsable] = (0, react.useState)(false);
749
+ const compact = require_icons.useLayoutEnv().size === "compact";
750
+ (0, react.useLayoutEffect)(() => {
751
+ const holder = probe.current?.parentElement;
752
+ if (holder && !holder.closest(HIDDEN_COPY_SELECTOR)) setUsable(true);
753
+ }, []);
754
+ if (!usable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
755
+ ref: probe,
756
+ hidden: true
757
+ });
758
+ return compact ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSheet, { ...props }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenMenu, { ...props });
759
+ }
760
+ function OpenMenu({ children, onClose, align = "start" }) {
761
+ const surface = (0, react.useRef)(null);
762
+ const edge = useBarEdge();
763
+ const nested = (0, react.useContext)(InMenuContext);
764
+ const close = (0, react.useRef)(onClose);
765
+ close.current = onClose;
766
+ (0, react.useLayoutEffect)(() => {
767
+ const menu = surface.current;
768
+ const holder = menu?.parentElement;
769
+ if (!menu || !holder) return;
770
+ return openMenu(menu, holder, {
771
+ placement: (direction) => nested ? submenuPlacementOf(direction) : menuPlacementOf(edge, direction, align, edge === "selection" ? selectionBarSideOf(holder) : void 0),
772
+ nested,
773
+ close: () => close.current()
774
+ });
775
+ }, []);
776
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
777
+ ref: surface,
778
+ ...POPOVER,
779
+ role: "menu",
780
+ part: "menu",
781
+ className: "border-border-subtle bg-elevated text-fg z-50 overflow-y-auto rounded-lg border shadow-xl",
782
+ style: {
783
+ position: "fixed",
784
+ right: "auto",
785
+ bottom: "auto",
786
+ margin: 0
787
+ },
788
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InMenuContext.Provider, {
789
+ value: true,
790
+ children
791
+ })
792
+ });
793
+ }
794
+ /** In a menu's sheet: how a row opens its submenu, over the menu. Null outside a sheet. */
795
+ const MenuSheetContext = (0, react.createContext)(null);
796
+ /** The button a menu's sheet opened from: the one beside the sheet that says it has a popup. */
797
+ function sheetTriggerOf(menu) {
798
+ const layer = menu.closest("[data-epdf-layer]");
799
+ const holder = layer?.parentElement;
800
+ return layer && holder ? triggerOf(holder, layer) : null;
801
+ }
802
+ /**
803
+ * A menu as a sheet: its rows in a sheet from the bottom of the viewer, which closes on a tap
804
+ * above it, Escape or a pick. A row that opens a menu slides that menu in over this one, with a
805
+ * row (or the arrow key back) that goes back to where it was.
806
+ */
807
+ function MenuSheet({ children, onClose }) {
808
+ const [submenus, setSubmenus] = (0, react.useState)([]);
809
+ const current = (0, react.useRef)(submenus);
810
+ current.current = submenus;
811
+ /** The row to give focus back to once the submenu over it has gone. */
812
+ const refocus = (0, react.useRef)(null);
813
+ const close = (0, react.useRef)(onClose);
814
+ close.current = onClose;
815
+ const open = (submenu) => setSubmenus([...current.current, submenu]);
816
+ const back = () => {
817
+ const shown = current.current;
818
+ if (shown.length === 0) return;
819
+ refocus.current = shown[shown.length - 1].opener;
820
+ setSubmenus(shown.slice(0, -1));
821
+ };
822
+ (0, react.useLayoutEffect)(() => {
823
+ refocus.current?.focus();
824
+ refocus.current = null;
825
+ }, [submenus]);
826
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_sheet.Sheet, {
827
+ label: "",
828
+ onClose,
829
+ trapFocus: false,
830
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuSheetContext.Provider, {
831
+ value: open,
832
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
833
+ className: "grid",
834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetMenu, {
835
+ covered: submenus.length > 0,
836
+ close: () => close.current(),
837
+ children
838
+ }), submenus.map((submenu, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SheetMenu, {
839
+ covered: i < submenus.length - 1,
840
+ close: back,
841
+ opener: submenu.opener,
842
+ label: submenu.label,
843
+ onBack: back,
844
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
845
+ menuId: submenu.menuId,
846
+ onRun: submenu.onRun
847
+ })
848
+ }, i))]
849
+ })
850
+ })
851
+ });
852
+ }
853
+ /**
854
+ * One menu in a menu's sheet: the first one, or a submenu over it with a row that goes back.
855
+ * It takes the menu keys while it's on top; covered, it can't take focus or presses.
856
+ */
857
+ function SheetMenu({ covered, close, opener, label, onBack, children }) {
858
+ const t = (0, _embedpdf_react_i18n.useT)();
859
+ const menu = (0, react.useRef)(null);
860
+ const actions = (0, react.useRef)({
861
+ close,
862
+ onBack
863
+ });
864
+ actions.current = {
865
+ close,
866
+ onBack
867
+ };
868
+ (0, react.useLayoutEffect)(() => {
869
+ const element = menu.current;
870
+ if (!element) return;
871
+ const trigger = opener ?? sheetTriggerOf(element);
872
+ const name = label ?? (trigger?.getAttribute("aria-label") || trigger?.getAttribute("title"));
873
+ if (name) element.setAttribute("aria-label", name);
874
+ const detachKeys = (0, _embedpdf_web.attachMenuKeys)(element, {
875
+ trigger,
876
+ close: () => actions.current.close(),
877
+ items: () => itemsOf(element),
878
+ initialFocus: "none"
879
+ });
880
+ queueMicrotask(() => {
881
+ if (element.isConnected && !element.inert) itemsOf(element).find((item) => !(0, _embedpdf_web.isUnavailable)(item))?.focus();
882
+ });
883
+ const keys = submenuKeysOf((0, _embedpdf_web.directionOf)(element));
884
+ const onKeyDown = (event) => {
885
+ if (event.defaultPrevented) return;
886
+ const item = event.target;
887
+ if (event.key === keys.open && item.getAttribute("aria-haspopup") === "menu") {
888
+ event.preventDefault();
889
+ item.click();
890
+ } else if (event.key === keys.close && actions.current.onBack) {
891
+ event.preventDefault();
892
+ actions.current.onBack();
893
+ }
894
+ };
895
+ element.addEventListener("keydown", onKeyDown);
896
+ return () => {
897
+ element.removeEventListener("keydown", onKeyDown);
898
+ detachKeys();
899
+ };
900
+ }, []);
901
+ (0, react.useLayoutEffect)(() => {
902
+ if (menu.current) menu.current.inert = covered;
903
+ }, [covered]);
904
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
905
+ ref: menu,
906
+ role: "menu",
907
+ part: "menu",
908
+ className: `bg-elevated col-start-1 row-start-1 min-w-0 ${onBack ? "starting:translate-x-full rtl:starting:-translate-x-full transition-[translate] duration-200 ease-out motion-reduce:transition-none" : ""}`,
909
+ children: [onBack && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
910
+ className: "p-1 pb-0",
911
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
912
+ type: "button",
913
+ tabIndex: -1,
914
+ role: "menuitem",
915
+ part: "menu-item",
916
+ "data-epdf-menu-back": "",
917
+ onClick: onBack,
918
+ className: ROW_CLASS,
919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
920
+ name: "chevronLeft",
921
+ size: 16,
922
+ className: "text-fg-muted rtl:-scale-x-100"
923
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
924
+ className: "flex-1 truncate",
925
+ children: t("menu.back")
926
+ })]
927
+ }), label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
928
+ className: "text-fg px-2.5 pb-1 pt-2 text-sm font-semibold",
929
+ children: label
930
+ })]
931
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InMenuContext.Provider, {
932
+ value: true,
933
+ children
934
+ })]
935
+ });
936
+ }
937
+ /**
938
+ * Which button each open menu opened from, per viewer (its shell). The shell
939
+ * keeps a menu open by its id, and several buttons can name one (the zoom menu
940
+ * in the main bar and in a floating bar): it opens at the button pressed, or
941
+ * at the first one drawn when code opened it.
942
+ */
943
+ const openers = /* @__PURE__ */ new WeakMap();
944
+ function openersOf(shell) {
945
+ let byMenu = openers.get(shell);
946
+ if (!byMenu) {
947
+ byMenu = /* @__PURE__ */ new Map();
948
+ openers.set(shell, byMenu);
949
+ }
950
+ return byMenu;
951
+ }
952
+ /**
953
+ * A button that opens the layout menu `menuId`, and the menu when it's open
954
+ * there. `children` draws the button, told whether its menu is open at it; the
955
+ * menu lines up with this wrapper (`className`), so it can hold more than the
956
+ * button, like the zoom field around its chevron.
957
+ */
958
+ function MenuAnchor({ menuId, className = "relative inline-flex", children }) {
959
+ const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
960
+ const open = (0, _embedpdf_react_shell.useShellState)((state) => state.openMenus.includes(menuId));
961
+ const anchor = (0, react.useRef)(null);
962
+ const [, claim] = (0, react.useReducer)((count) => count + 1, 0);
963
+ const opener = shell ? openersOf(shell).get(menuId) : void 0;
964
+ const openHere = open && opener !== void 0 && opener === anchor.current;
965
+ (0, react.useLayoutEffect)(() => {
966
+ const element = anchor.current;
967
+ if (!shell || !element) return;
968
+ const byMenu = openersOf(shell);
969
+ const current = byMenu.get(menuId);
970
+ if (!open) {
971
+ if (current === element) byMenu.delete(menuId);
972
+ return;
973
+ }
974
+ if (!current?.isConnected && !element.closest(HIDDEN_COPY_SELECTOR)) {
975
+ byMenu.set(menuId, element);
976
+ claim();
977
+ }
978
+ });
979
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
980
+ ref: anchor,
981
+ className,
982
+ onClickCapture: (event) => {
983
+ const target = event.target;
984
+ const pressedButton = target.closest("[aria-haspopup]") && !target.closest("[role=\"menu\"]");
985
+ if (shell && anchor.current && pressedButton) openersOf(shell).set(menuId, anchor.current);
986
+ },
987
+ children: [children(openHere), openHere && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
988
+ onClose: () => shell?.closeMenu(menuId),
989
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
990
+ menuId,
991
+ onRun: () => shell?.closeMenu(menuId)
992
+ })
993
+ })]
994
+ });
995
+ }
996
+ const choiceOf = (rows) => {
997
+ const choosable = rows.filter((row) => !row.menu);
998
+ const active = choosable.filter((row) => row.active).length;
999
+ if (active === 0) return "none";
1000
+ return active === 1 && choosable.length > 1 ? "radio" : "checkbox";
1001
+ };
1002
+ const ROLE_OF = {
1003
+ none: "menuitem",
1004
+ radio: "menuitemradio",
1005
+ checkbox: "menuitemcheckbox"
1006
+ };
1007
+ const ROW_BASE = "flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-start text-sm outline-none transition-colors aria-disabled:cursor-not-allowed aria-disabled:opacity-40";
1008
+ /** A row, the pointer over it and the keyboard on it: the neutral hover look. */
1009
+ const ROW_CLASS = `${ROW_BASE} text-fg-secondary hover:bg-hover focus-visible:bg-hover aria-disabled:hover:bg-transparent`;
1010
+ /**
1011
+ * The current choice: the accent look of an active toolbar button, so "on" reads the same
1012
+ * everywhere and never looks like the row the keyboard is on. The keyboard on it adds an inset
1013
+ * ring; with forced colors, where backgrounds go, an outline marks it.
1014
+ */
1015
+ const CHOSEN_ROW_CLASS = `${ROW_BASE} bg-selected text-accent focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent forced-colors:outline-2 forced-colors:outline-solid forced-colors:-outline-offset-2`;
1016
+ /**
1017
+ * One row: the command's icon, label and shortcut, in the accent look when it
1018
+ * is the current choice. A row that can't run says why in its tooltip, and
1019
+ * stays where it is, greyed out.
1020
+ */
1021
+ function MenuItem({ command, choice, onSelect }) {
1022
+ const checked = choice !== "none" && command.active;
1023
+ const [shortcut] = command.shortcuts;
1024
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1025
+ type: "button",
1026
+ tabIndex: -1,
1027
+ role: ROLE_OF[choice],
1028
+ "aria-checked": choice === "none" ? void 0 : command.active,
1029
+ "aria-disabled": command.enabled ? void 0 : true,
1030
+ title: command.enabled ? void 0 : command.disabledReason,
1031
+ part: checked ? "menu-item menu-item-active" : "menu-item",
1032
+ onClick: () => {
1033
+ if (command.enabled) onSelect();
1034
+ },
1035
+ className: checked ? CHOSEN_ROW_CLASS : ROW_CLASS,
1036
+ children: [
1037
+ command.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1038
+ name: command.icon,
1039
+ size: 16,
1040
+ accent: command.iconAccent,
1041
+ className: checked ? "text-accent" : "text-fg-muted"
1042
+ }),
1043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1044
+ className: "flex-1 truncate",
1045
+ children: command.label
1046
+ }),
1047
+ shortcut && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1048
+ className: "text-fg-muted ms-4 text-xs",
1049
+ children: (0, _embedpdf_react_commands.formatShortcut)(shortcut)
1050
+ })
1051
+ ]
1052
+ });
1053
+ }
1054
+ /**
1055
+ * A row whose command opens a menu: it opens that menu beside it, with a
1056
+ * click or the arrow key toward it. Picking a row in there runs `onRun` too,
1057
+ * so the whole menu closes.
1058
+ */
1059
+ function SubmenuItem({ command, menuId, onRun }) {
1060
+ const [open, setOpen] = (0, react.useState)(false);
1061
+ const openInSheet = (0, react.useContext)(MenuSheetContext);
1062
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1063
+ role: "none",
1064
+ className: "relative",
1065
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1066
+ type: "button",
1067
+ tabIndex: -1,
1068
+ role: "menuitem",
1069
+ "aria-haspopup": "menu",
1070
+ "aria-expanded": open,
1071
+ "aria-disabled": command.enabled ? void 0 : true,
1072
+ title: command.enabled ? void 0 : command.disabledReason,
1073
+ part: "menu-item",
1074
+ onClick: (event) => {
1075
+ if (!command.enabled) return;
1076
+ if (openInSheet) openInSheet({
1077
+ menuId,
1078
+ label: command.label,
1079
+ opener: event.currentTarget,
1080
+ onRun
1081
+ });
1082
+ else setOpen((previous) => !previous);
1083
+ },
1084
+ className: `${ROW_CLASS} ${open ? "bg-hover" : ""}`,
1085
+ children: [
1086
+ command.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1087
+ name: command.icon,
1088
+ size: 16,
1089
+ accent: command.iconAccent,
1090
+ className: "text-fg-muted"
1091
+ }),
1092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1093
+ className: "flex-1 truncate",
1094
+ children: command.label
1095
+ }),
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1097
+ name: "chevronRight",
1098
+ size: 16,
1099
+ className: "text-fg-muted rtl:-scale-x-100"
1100
+ })
1101
+ ]
1102
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
1103
+ onClose: () => setOpen(false),
1104
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuBody, {
1105
+ menuId,
1106
+ onRun
1107
+ })
1108
+ })]
1109
+ });
1110
+ }
1111
+ /** A layout menu's row, by command id. */
1112
+ function MenuRow({ commandId, choice, onRun }) {
1113
+ const command = (0, _embedpdf_react_commands.useCommand)(commandId);
1114
+ if (!command.visible) return null;
1115
+ if (command.menu) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubmenuItem, {
1116
+ command,
1117
+ menuId: command.menu,
1118
+ onRun
1119
+ });
1120
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
1121
+ command,
1122
+ choice,
1123
+ onSelect: () => {
1124
+ command.run();
1125
+ onRun?.();
1126
+ }
1127
+ });
1128
+ }
1129
+ /** A section: its rows as a group named by its title, after a line when it isn't the first. */
1130
+ function MenuGroup({ title, first, children }) {
1131
+ const titleId = (0, react.useId)();
1132
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!first && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1133
+ role: "separator",
1134
+ className: "bg-border-subtle my-1 h-px"
1135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1136
+ role: "group",
1137
+ "aria-labelledby": title ? titleId : void 0,
1138
+ children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1139
+ id: titleId,
1140
+ className: "text-fg-muted px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide",
1141
+ children: title
1142
+ }), children]
1143
+ })] });
1144
+ }
1145
+ const sameChoices = (left, right) => left.length === right.length && left.every((choice, i) => choice === right[i]);
1146
+ /** One section-separated menu, by its id in the layout. */
1147
+ function MenuBody({ menuId, onRun }) {
1148
+ const t = (0, _embedpdf_react_i18n.useT)();
1149
+ const menu = require_icons.useMenu(menuId);
1150
+ const commands = (0, _embedpdf_react_commands.useCommands)();
1151
+ const documentId = (0, _embedpdf_react_runtime.useDocumentId)() ?? void 0;
1152
+ const choices = (0, _embedpdf_react_runtime.useKernelValue)(() => (menu ?? []).map((section) => {
1153
+ const rows = section.items.map((id) => commands.resolveCommand(id, documentId)).filter((command) => command?.visible === true);
1154
+ return rows.length === 0 ? null : choiceOf(rows);
1155
+ }), sameChoices);
1156
+ if (!menu) return null;
1157
+ const shown = menu.flatMap((section, i) => {
1158
+ const choice = choices[i];
1159
+ return choice ? [{
1160
+ section,
1161
+ choice
1162
+ }] : [];
1163
+ });
1164
+ if (shown.length === 0) return null;
1165
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1166
+ className: "min-w-52 p-1",
1167
+ children: shown.map(({ section, choice }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuGroup, {
1168
+ first: i === 0,
1169
+ title: section.title ?? (section.titleKey ? t(section.titleKey) : void 0),
1170
+ children: section.items.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuRow, {
1171
+ commandId: id,
1172
+ choice,
1173
+ onRun
1174
+ }, id))
1175
+ }, i))
1176
+ });
1177
+ }
1178
+ /**
1179
+ * A menu the toolbar derives (the "More" menu, a folded group's): its
1180
+ * sections in bar order, the bar's groups as titled sections, and a row for
1181
+ * each command; a command that opens a menu opens it beside its row.
1182
+ */
1183
+ function ToolbarMenu({ view, align }) {
1184
+ const t = (0, _embedpdf_react_i18n.useT)();
1185
+ if (!view.isOpen) return null;
1186
+ const sections = view.sections.map((section) => {
1187
+ const rows = section.rows.flatMap((row) => {
1188
+ const command = view.resolve(row.command);
1189
+ return command ? [{
1190
+ row,
1191
+ command
1192
+ }] : [];
1193
+ });
1194
+ return {
1195
+ section,
1196
+ rows,
1197
+ choice: section.role === "radio" ? "radio" : choiceOf(rows.map(({ command }) => command))
1198
+ };
1199
+ });
1200
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuPopover, {
1201
+ onClose: view.close,
1202
+ align,
1203
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1204
+ className: "min-w-56 p-1",
1205
+ children: sections.map(({ section, rows, choice }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuGroup, {
1206
+ first: i === 0,
1207
+ title: section.labelKey ? t(section.labelKey) : void 0,
1208
+ children: rows.map(({ row, command }) => row.type === "submenu" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubmenuItem, {
1209
+ command,
1210
+ menuId: row.menu,
1211
+ onRun: view.close
1212
+ }, row.command) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
1213
+ command,
1214
+ choice,
1215
+ onSelect: () => {
1216
+ view.execute(row.command);
1217
+ view.close();
1218
+ }
1219
+ }, row.command))
1220
+ }, i))
1221
+ })
1222
+ });
1223
+ }
1224
+ //#endregion
1225
+ //#region src/ui/toolbar.tsx
1226
+ /**
1227
+ * The app's toolbar look — Tailwind render props over the headless <Toolbar>.
1228
+ * The app owns the pixels; ui-core owns the physics (measure → solve →
1229
+ * overflow). Nothing here knows about breakpoints or widths.
1230
+ */
1231
+ const BTN_BASE = "flex h-8 w-auto min-w-8 cursor-pointer items-center justify-center rounded-md p-[5px] transition-colors";
1232
+ const BTN_HOVER = "hover:bg-hover hover:ring hover:ring-accent";
1233
+ const BTN_ACTIVE = "border-none bg-selected text-accent shadow ring ring-accent";
1234
+ const BTN_DISABLED = "cursor-not-allowed opacity-50 hover:bg-transparent hover:ring-0";
1235
+ const buttonClass = (active, enabled = true) => `${BTN_BASE} ${active ? BTN_ACTIVE : BTN_HOVER} ${enabled ? "" : BTN_DISABLED}`;
1236
+ /**
1237
+ * The part name of a button in a bar, for page CSS through `::part()`: `toolbar-button`, and
1238
+ * `toolbar-button-active` too while it's pressed or its menu is open. Part names are public, as
1239
+ * the Theming page lists them.
1240
+ */
1241
+ const buttonPart = (active) => active ? "toolbar-button toolbar-button-active" : "toolbar-button";
1242
+ /** The part name of a mode tab: `mode-tab`, and `mode-tab-active` too for the open mode. */
1243
+ const modeTabPart = (active) => active ? "mode-tab mode-tab-active" : "mode-tab";
1244
+ 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";
1245
+ const TAB_ACTIVE = "border-b-accent text-accent hover:border-b-accent";
1246
+ const TAB_INACTIVE = "border-b-transparent";
1247
+ const tabClass = (active, enabled = true) => `${TAB_BASE} ${active ? TAB_ACTIVE : TAB_INACTIVE} ${enabled ? "" : "cursor-not-allowed opacity-50"}`;
1248
+ function CommandButton({ cmd, variant, run }) {
1249
+ const isTab = variant === "label";
1250
+ const draw = (active) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1251
+ type: "button",
1252
+ onClick: run,
1253
+ disabled: !cmd.enabled,
1254
+ "aria-pressed": cmd.menu ? void 0 : active || void 0,
1255
+ "aria-haspopup": cmd.menu ? "menu" : void 0,
1256
+ "aria-expanded": cmd.menu ? active : void 0,
1257
+ "aria-label": cmd.label,
1258
+ title: cmd.disabledReason ?? cmd.label,
1259
+ part: isTab ? modeTabPart(active) : buttonPart(active),
1260
+ className: isTab ? tabClass(active, cmd.enabled) : buttonClass(active, cmd.enabled),
1261
+ children: isTab ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1262
+ className: "whitespace-nowrap px-1",
1263
+ children: cmd.label
1264
+ }) : variant === "icon+label" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1265
+ className: "flex items-center whitespace-nowrap text-sm",
1266
+ children: [cmd.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1267
+ name: cmd.icon,
1268
+ size: 20,
1269
+ accent: cmd.iconAccent,
1270
+ className: "me-1.5 shrink-0"
1271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: cmd.label })]
1272
+ }) : cmd.icon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1273
+ name: cmd.icon,
1274
+ size: 20,
1275
+ accent: cmd.iconAccent
1276
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1277
+ className: "text-sm",
1278
+ children: cmd.label
1279
+ })
1280
+ });
1281
+ if (!cmd.menu) return draw(cmd.active);
1282
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
1283
+ menuId: cmd.menu,
1284
+ children: draw
1285
+ });
1286
+ }
1287
+ /**
1288
+ * Makes a control inside an item one more stop of the toolbar's arrow keys: the zoom field's
1289
+ * buttons are reached with the arrows, like the bar's own, and the bar stays one Tab stop.
1290
+ */
1291
+ const ARROW_STOP = {
1292
+ [_embedpdf_web.TOOLBAR_ITEM_ATTRIBUTE]: "",
1293
+ className: "contents"
1294
+ };
1295
+ function ZoomControls() {
1296
+ const t = (0, _embedpdf_react_i18n.useT)();
1297
+ const commands = (0, _embedpdf_react_commands.useCommands)();
1298
+ const zoomIn = (0, _embedpdf_react_commands.useCommand)("zoom:in");
1299
+ const zoomOut = (0, _embedpdf_react_commands.useCommand)("zoom:out");
1300
+ const zoomMenu = (0, _embedpdf_react_commands.useCommand)("menu:zoom");
1301
+ const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
1302
+ const stage = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_stage.StageToken);
1303
+ const level = (0, _embedpdf_react_runtime.useOptionalSelector)(_embedpdf_react_stage.StageToken, (stage) => stage.getZoomLevel(), 1);
1304
+ const pct = Math.round((level ?? 1) * 100);
1305
+ const [inputValue, setInputValue] = (0, react.useState)(String(pct));
1306
+ (0, react.useEffect)(() => setInputValue(String(pct)), [pct]);
1307
+ const submit = (event) => {
1308
+ event.preventDefault();
1309
+ const value = parseFloat(inputValue);
1310
+ if (!isNaN(value) && value > 0) stage?.zoomTo({ level: value / 100 });
1311
+ };
1312
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
1313
+ menuId: "zoom",
1314
+ className: "relative block",
1315
+ children: (zoomOpen) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1316
+ className: "bg-hover flex items-center rounded",
1317
+ children: [
1318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1319
+ onSubmit: submit,
1320
+ className: "flex min-w-0 flex-nowrap items-center overflow-hidden whitespace-nowrap",
1321
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1322
+ name: "zoom",
1323
+ type: "text",
1324
+ inputMode: "numeric",
1325
+ pattern: "\\d*",
1326
+ "aria-label": t("commands.zoom.level"),
1327
+ className: "h-6 w-8 min-w-0 shrink border-0 bg-transparent p-0 text-end text-sm outline-none focus:outline-none",
1328
+ value: inputValue,
1329
+ onChange: (event) => setInputValue(event.target.value.replace(/[^0-9]/g, "")),
1330
+ onBlur: () => {
1331
+ if (!inputValue || parseFloat(inputValue) <= 0) setInputValue(String(pct));
1332
+ }
1333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1334
+ className: "shrink-0 text-sm",
1335
+ children: "%"
1336
+ })]
1337
+ }),
1338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1339
+ ...ARROW_STOP,
1340
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1341
+ type: "button",
1342
+ "aria-haspopup": "menu",
1343
+ "aria-expanded": zoomOpen,
1344
+ title: zoomMenu.label,
1345
+ part: buttonPart(false),
1346
+ className: buttonClass(false),
1347
+ onClick: () => shell?.toggleMenu("zoom"),
1348
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1349
+ name: "chevronDown",
1350
+ size: 20
1351
+ })
1352
+ })
1353
+ }),
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1355
+ ...ARROW_STOP,
1356
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1357
+ type: "button",
1358
+ title: zoomOut.label,
1359
+ part: buttonPart(false),
1360
+ className: buttonClass(false),
1361
+ onClick: () => commands.execute("zoom:out"),
1362
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1363
+ name: "zoomOut",
1364
+ size: 20
1365
+ })
1366
+ })
1367
+ }),
1368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1369
+ ...ARROW_STOP,
1370
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1371
+ type: "button",
1372
+ title: zoomIn.label,
1373
+ part: buttonPart(false),
1374
+ className: buttonClass(false),
1375
+ onClick: () => commands.execute("zoom:in"),
1376
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1377
+ name: "zoomIn",
1378
+ size: 20
1379
+ })
1380
+ })
1381
+ })
1382
+ ]
1383
+ })
1384
+ });
1385
+ }
1386
+ /** The 'button' variant of the zoom slot — a single icon that opens the menu. */
1387
+ function ZoomButton() {
1388
+ const shell = (0, _embedpdf_react_runtime.useOptionalCapability)(_embedpdf_react_shell.ShellToken);
1389
+ const zoomMenu = (0, _embedpdf_react_commands.useCommand)("menu:zoom");
1390
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuAnchor, {
1391
+ menuId: "zoom",
1392
+ children: (zoomOpen) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1393
+ type: "button",
1394
+ title: zoomMenu.label,
1395
+ "aria-haspopup": "menu",
1396
+ "aria-expanded": zoomOpen,
1397
+ part: buttonPart(zoomOpen),
1398
+ className: buttonClass(zoomOpen),
1399
+ onClick: () => shell?.toggleMenu("zoom"),
1400
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1401
+ name: "zoomIn",
1402
+ size: 20
1403
+ })
1404
+ })
1405
+ });
1406
+ }
1407
+ function GroupTrigger({ view }) {
1408
+ const [open, setOpen] = (0, react.useState)(false);
1409
+ const activeHidden = view.commands.some((command) => command.active);
1410
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1411
+ className: "relative inline-flex",
1412
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1413
+ type: "button",
1414
+ onClick: () => setOpen((previous) => !previous),
1415
+ "aria-haspopup": "menu",
1416
+ "aria-expanded": open,
1417
+ part: view.id === "modes" ? modeTabPart(activeHidden) : buttonPart(open),
1418
+ className: tabClass(activeHidden),
1419
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1420
+ name: "chevronDown",
1421
+ size: 18
1422
+ })
1423
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
1424
+ });
1425
+ }
1426
+ function CollapsedModes({ view }) {
1427
+ const [open, setOpen] = (0, react.useState)(false);
1428
+ const active = view.commands.find((command) => command.active) ?? view.commands[0];
1429
+ const isActive = Boolean(active?.active && active.id !== _embedpdf_core_ui.VIEW_MODE_COMMAND);
1430
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1431
+ style: { width: 100 },
1432
+ className: "relative",
1433
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1434
+ type: "button",
1435
+ onClick: () => setOpen((previous) => !previous),
1436
+ "aria-haspopup": "menu",
1437
+ "aria-expanded": open,
1438
+ part: buttonPart(open),
1439
+ className: "bg-surface hover:bg-hover flex w-full cursor-pointer appearance-none items-center rounded-md py-1.5 pe-2 ps-3 text-[13px] transition-colors",
1440
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1441
+ className: `min-w-0 flex-1 truncate text-start ${isActive ? "text-accent" : "text-fg"}`,
1442
+ children: active?.label
1443
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1444
+ name: "chevronDown",
1445
+ size: 16,
1446
+ className: isActive ? "text-accent" : "text-fg-secondary"
1447
+ })]
1448
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
1449
+ });
1450
+ }
1451
+ function CollapsedMenu({ view }) {
1452
+ const t = (0, _embedpdf_react_i18n.useT)();
1453
+ const [open, setOpen] = (0, react.useState)(false);
1454
+ const shown = view.commands.find((command) => command.active) ?? view.commands[0];
1455
+ const label = view.labelKey ? t(view.labelKey) : shown?.label;
1456
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1457
+ className: "relative inline-flex",
1458
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1459
+ type: "button",
1460
+ onClick: () => setOpen((previous) => !previous),
1461
+ "aria-haspopup": "menu",
1462
+ "aria-expanded": open,
1463
+ "aria-label": label,
1464
+ title: label,
1465
+ part: buttonPart(open),
1466
+ className: buttonClass(open),
1467
+ children: [shown?.icon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1468
+ name: shown.icon,
1469
+ size: 20,
1470
+ accent: shown.iconAccent
1471
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1472
+ className: "whitespace-nowrap px-1 text-sm",
1473
+ children: label
1474
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1475
+ name: "chevronDown",
1476
+ size: 14,
1477
+ className: "text-fg-secondary"
1478
+ })]
1479
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, { view: (0, _embedpdf_core_ui.groupMenuView)(view, open, () => setOpen(false)) })]
1480
+ });
1481
+ }
1482
+ function OverflowMenu({ view }) {
1483
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarMenu, {
1484
+ view,
1485
+ align: "end"
1486
+ });
1487
+ }
1488
+ function OverflowTrigger({ isOpen, toggle }) {
1489
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1490
+ type: "button",
1491
+ onClick: toggle,
1492
+ "aria-haspopup": "menu",
1493
+ "aria-expanded": isOpen,
1494
+ title: (0, _embedpdf_react_i18n.useT)()("demo.more"),
1495
+ part: buttonPart(isOpen),
1496
+ className: buttonClass(isOpen),
1497
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons.Icon, {
1498
+ name: "dots",
1499
+ size: 20
1500
+ })
1501
+ });
1502
+ }
1503
+ /** The custom items in `bar`, in order. */
1504
+ function customItemsOf(bar) {
1505
+ return [
1506
+ ...bar.sections.start ?? [],
1507
+ ...bar.sections.center ?? [],
1508
+ ...bar.sections.end ?? []
1509
+ ].flatMap((group) => group.items.filter((child) => typeof child === "object" && "slot" in child));
1510
+ }
1511
+ /** What your item stands for while nothing is slotted into it: its command's button. */
1512
+ function StandIn({ commandId }) {
1513
+ const command = (0, _embedpdf_react_commands.useCommand)(commandId);
1514
+ if (!command.visible) return null;
1515
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandButton, {
1516
+ cmd: command,
1517
+ variant: "icon",
1518
+ run: () => void command.run()
1519
+ });
1520
+ }
1521
+ function AppToolbar({ bar, orientation = "horizontal", className }) {
1522
+ const vertical = orientation === "vertical";
1523
+ const builtIn = {
1524
+ "measurement-scale": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MeasurementScaleButton, {}),
1525
+ zoom: (variant) => variant === "button" || vertical ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomButton, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomControls, {}),
1526
+ "quick-stamps": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickStamps, {}),
1527
+ "page-number": () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageNumber, {}),
1528
+ "view-only": (variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewOnlyLabel, { compact: variant === "icon" || vertical })
1529
+ };
1530
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_embedpdf_react_toolbar.Toolbar, {
1531
+ bar,
1532
+ orientation,
1533
+ gap: 8,
1534
+ separatorWidth: 1,
1535
+ className,
1536
+ renderCommand: (cmd, variant, run) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandButton, {
1537
+ cmd,
1538
+ variant,
1539
+ run
1540
+ }),
1541
+ renderCustom: {
1542
+ ...Object.fromEntries(customItemsOf(bar).filter((item) => !Object.prototype.hasOwnProperty.call(builtIn, item.slot)).map((item) => [item.slot, (variant, ctx) => ctx.layer === "measure" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ItemSocket, {
1543
+ id: item.slot,
1544
+ variant: item.variants ? variant : null,
1545
+ orientation,
1546
+ measure: ctx.measure,
1547
+ children: item.terminal ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StandIn, { commandId: item.terminal }) : null
1548
+ })])),
1549
+ ...builtIn
1550
+ },
1551
+ renderCollapsed: (view) => view.id === "modes" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedModes, { view }) : view.collapse === "menu" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsedMenu, { view }) : void 0,
1552
+ renderGroupTrigger: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupTrigger, { view }),
1553
+ renderSeparator: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `bg-border self-center ${vertical ? "h-px w-6" : "h-6 w-px"}` }),
1554
+ renderOverflowTrigger: (isOpen, toggle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverflowTrigger, {
1555
+ isOpen,
1556
+ toggle
1557
+ }),
1558
+ renderOverflowMenu: (view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverflowMenu, { view })
1559
+ });
1560
+ }
1561
+ //#endregion
1562
+ Object.defineProperty(exports, "AppToolbar", {
1563
+ enumerable: true,
1564
+ get: function() {
1565
+ return AppToolbar;
1566
+ }
1567
+ });
1568
+ Object.defineProperty(exports, "BarEdgeProvider", {
1569
+ enumerable: true,
1570
+ get: function() {
1571
+ return BarEdgeProvider;
1572
+ }
1573
+ });
1574
+ Object.defineProperty(exports, "CommandButton", {
1575
+ enumerable: true,
1576
+ get: function() {
1577
+ return CommandButton;
1578
+ }
1579
+ });
1580
+ Object.defineProperty(exports, "MenuAnchor", {
1581
+ enumerable: true,
1582
+ get: function() {
1583
+ return MenuAnchor;
1584
+ }
1585
+ });
1586
+ Object.defineProperty(exports, "MenuItem", {
1587
+ enumerable: true,
1588
+ get: function() {
1589
+ return MenuItem;
1590
+ }
1591
+ });
1592
+ Object.defineProperty(exports, "MenuPopover", {
1593
+ enumerable: true,
1594
+ get: function() {
1595
+ return MenuPopover;
1596
+ }
1597
+ });
1598
+ Object.defineProperty(exports, "SlottedEdgesProvider", {
1599
+ enumerable: true,
1600
+ get: function() {
1601
+ return SlottedEdgesProvider;
1602
+ }
1603
+ });
1604
+ Object.defineProperty(exports, "TOOLBAR_ITEM_CONTEXT_ATTRIBUTES", {
1605
+ enumerable: true,
1606
+ get: function() {
1607
+ return TOOLBAR_ITEM_CONTEXT_ATTRIBUTES;
1608
+ }
1609
+ });
1610
+ Object.defineProperty(exports, "bothAllow", {
1611
+ enumerable: true,
1612
+ get: function() {
1613
+ return bothAllow;
1614
+ }
1615
+ });
1616
+ Object.defineProperty(exports, "buttonClass", {
1617
+ enumerable: true,
1618
+ get: function() {
1619
+ return buttonClass;
1620
+ }
1621
+ });
1622
+ Object.defineProperty(exports, "buttonPart", {
1623
+ enumerable: true,
1624
+ get: function() {
1625
+ return buttonPart;
1626
+ }
1627
+ });
1628
+ Object.defineProperty(exports, "byHandSettingsOf", {
1629
+ enumerable: true,
1630
+ get: function() {
1631
+ return byHandSettingsOf;
1632
+ }
1633
+ });
1634
+ Object.defineProperty(exports, "createSlottedEdges", {
1635
+ enumerable: true,
1636
+ get: function() {
1637
+ return createSlottedEdges;
1638
+ }
1639
+ });
1640
+ Object.defineProperty(exports, "disabledSettingsOf", {
1641
+ enumerable: true,
1642
+ get: function() {
1643
+ return disabledSettingsOf;
1644
+ }
1645
+ });
1646
+ Object.defineProperty(exports, "sameByHand", {
1647
+ enumerable: true,
1648
+ get: function() {
1649
+ return sameByHand;
1650
+ }
1651
+ });
1652
+ Object.defineProperty(exports, "useSlottedEdges", {
1653
+ enumerable: true,
1654
+ get: function() {
1655
+ return useSlottedEdges;
1656
+ }
1657
+ });
1658
+
1659
+ //# sourceMappingURL=toolbar-CJu2wKTU.cjs.map