@june-ai/june-ui 0.2.4 → 0.2.6

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 CHANGED
@@ -6,19 +6,28 @@ import {
6
6
  docToHtml,
7
7
  docToMarkdown,
8
8
  emptyDoc,
9
+ getPageCount,
10
+ getSearchState,
11
+ gotoMatch,
9
12
  isJuneError,
10
13
  parseDocumentContent,
14
+ remeasurePagination,
15
+ replaceAll,
16
+ replaceCurrent,
11
17
  sanitizeHref,
18
+ setPaginationConfig,
19
+ setSearch,
12
20
  slugify,
13
21
  toJuneError,
14
22
  wordCountOf
15
- } from "./chunk-HFHRSNIL.js";
23
+ } from "./chunk-VFZI3SEL.js";
16
24
 
17
25
  // src/renderer/renderer.tsx
18
26
  import { Renderer } from "@openuidev/react-lang";
19
27
  import {
20
28
  Fragment,
21
29
  useCallback,
30
+ useEffect,
22
31
  useMemo as useMemo2,
23
32
  useRef
24
33
  } from "react";
@@ -96,6 +105,316 @@ function useDocumentValue(store, id, selector) {
96
105
  return useSyncExternalStore(store.subscribe, read, read);
97
106
  }
98
107
 
108
+ // src/components/document-editor/document-merge.ts
109
+ var MAX_CELLS = 4e6;
110
+ function matchIndices(a, b) {
111
+ const n = a.length;
112
+ const m = b.length;
113
+ if (n * m > MAX_CELLS) return null;
114
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
115
+ for (let i2 = n - 1; i2 >= 0; i2--) {
116
+ for (let j2 = m - 1; j2 >= 0; j2--) {
117
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
118
+ }
119
+ }
120
+ const matches = /* @__PURE__ */ new Map();
121
+ let i = 0;
122
+ let j = 0;
123
+ while (i < n && j < m) {
124
+ if (a[i] === b[j]) {
125
+ matches.set(i, j);
126
+ i++;
127
+ j++;
128
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
129
+ else j++;
130
+ }
131
+ return matches;
132
+ }
133
+ var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
134
+ function mergeBlocks(base, local, incoming) {
135
+ if (same(local, base)) return { blocks: incoming, conflicts: 0 };
136
+ if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
137
+ const ml = matchIndices(base, local);
138
+ const mn = matchIndices(base, incoming);
139
+ if (!ml || !mn) return { blocks: null, conflicts: 1 };
140
+ const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
141
+ const out = [];
142
+ let conflicts = 0;
143
+ let pb = -1;
144
+ let pl = -1;
145
+ let pn = -1;
146
+ const region = (bEnd, lEnd, nEnd) => {
147
+ const B = base.slice(pb + 1, bEnd);
148
+ const L = local.slice(pl + 1, lEnd);
149
+ const N = incoming.slice(pn + 1, nEnd);
150
+ if (B.length === L.length && L.length === N.length) {
151
+ for (let k = 0; k < B.length; k++) {
152
+ if (L[k] === B[k]) out.push(N[k]);
153
+ else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
154
+ else {
155
+ conflicts++;
156
+ out.push(L[k]);
157
+ }
158
+ }
159
+ return;
160
+ }
161
+ if (same(L, B)) out.push(...N);
162
+ else if (same(N, B) || same(L, N)) out.push(...L);
163
+ else {
164
+ conflicts++;
165
+ out.push(...L);
166
+ }
167
+ };
168
+ for (const a of anchors) {
169
+ const la = ml.get(a);
170
+ const na = mn.get(a);
171
+ region(a, la, na);
172
+ out.push(local[la]);
173
+ pb = a;
174
+ pl = la;
175
+ pn = na;
176
+ }
177
+ region(base.length, local.length, incoming.length);
178
+ return { blocks: out, conflicts };
179
+ }
180
+ function mergeDocuments(base, local, incoming) {
181
+ const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
182
+ const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
183
+ if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
184
+ if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
185
+ return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
186
+ }
187
+
188
+ // src/components/document-editor/document-store.ts
189
+ var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
190
+ function createDocumentStore() {
191
+ const records = /* @__PURE__ */ new Map();
192
+ const listeners = /* @__PURE__ */ new Set();
193
+ const emit = () => listeners.forEach((listener) => listener());
194
+ const put = (record) => {
195
+ records.set(record.id, record);
196
+ emit();
197
+ };
198
+ const fresh2 = (doc) => ({
199
+ id: doc.id,
200
+ title: doc.title,
201
+ documentType: doc.documentType,
202
+ status: doc.status,
203
+ json: doc.json,
204
+ baseJson: doc.json,
205
+ baseTitle: doc.title,
206
+ lastIncomingKey: incomingKey(doc),
207
+ revision: 0,
208
+ origin: "external",
209
+ pending: null,
210
+ updatedAt: Date.now()
211
+ });
212
+ return {
213
+ get: (id) => records.get(id),
214
+ list: () => [...records.values()],
215
+ subscribe: (listener) => {
216
+ listeners.add(listener);
217
+ return () => {
218
+ listeners.delete(listener);
219
+ };
220
+ },
221
+ receive(incoming) {
222
+ const current = records.get(incoming.id);
223
+ if (!current) {
224
+ put(fresh2(incoming));
225
+ return "created";
226
+ }
227
+ const key = incomingKey(incoming);
228
+ if (current.lastIncomingKey === key) return "unchanged";
229
+ const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
230
+ const userEditedTitle = current.title !== current.baseTitle;
231
+ const title = userEditedTitle ? current.title : incoming.title;
232
+ const meta = {
233
+ documentType: incoming.documentType ?? current.documentType,
234
+ status: incoming.status ?? current.status
235
+ };
236
+ if (!userEditedBody) {
237
+ put({
238
+ ...current,
239
+ ...meta,
240
+ title,
241
+ baseTitle: incoming.title,
242
+ json: incoming.json,
243
+ baseJson: incoming.json,
244
+ lastIncomingKey: key,
245
+ revision: current.revision + 1,
246
+ origin: "external",
247
+ pending: null,
248
+ updatedAt: Date.now()
249
+ });
250
+ return "applied";
251
+ }
252
+ const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
253
+ if (conflicts === 0) {
254
+ put({
255
+ ...current,
256
+ ...meta,
257
+ title,
258
+ baseTitle: incoming.title,
259
+ json: doc,
260
+ baseJson: incoming.json,
261
+ lastIncomingKey: key,
262
+ revision: current.revision + 1,
263
+ origin: "external",
264
+ pending: null,
265
+ updatedAt: Date.now()
266
+ });
267
+ return "merged";
268
+ }
269
+ put({
270
+ ...current,
271
+ ...meta,
272
+ lastIncomingKey: key,
273
+ pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
274
+ });
275
+ return "conflict";
276
+ },
277
+ hydrate(doc) {
278
+ put(fresh2(doc));
279
+ },
280
+ edit(id, json, origin) {
281
+ const current = records.get(id);
282
+ if (!current || docKey(current.json) === docKey(json)) return;
283
+ put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
284
+ },
285
+ setTitle(id, title) {
286
+ const current = records.get(id);
287
+ if (!current || current.title === title) return;
288
+ put({ ...current, title, updatedAt: Date.now() });
289
+ },
290
+ resolve(id, choice) {
291
+ const current = records.get(id);
292
+ const pending = current?.pending;
293
+ if (!current || !pending) return;
294
+ if (choice === "keep") {
295
+ put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
296
+ return;
297
+ }
298
+ const json = choice === "merge" ? pending.merged : pending.json;
299
+ const title = choice === "merge" ? current.title : pending.title;
300
+ put({
301
+ ...current,
302
+ title,
303
+ json,
304
+ baseJson: pending.json,
305
+ baseTitle: pending.title,
306
+ revision: current.revision + 1,
307
+ origin: "external",
308
+ pending: null,
309
+ updatedAt: Date.now()
310
+ });
311
+ },
312
+ remove(id) {
313
+ if (records.delete(id)) emit();
314
+ }
315
+ };
316
+ }
317
+ var echoes = /* @__PURE__ */ new WeakMap();
318
+ var ECHO_MEMORY = 8;
319
+ function rememberDocumentEcho(store, id, content) {
320
+ let byId = echoes.get(store);
321
+ if (!byId) echoes.set(store, byId = /* @__PURE__ */ new Map());
322
+ const list2 = byId.get(id) ?? [];
323
+ list2.push(content);
324
+ if (list2.length > ECHO_MEMORY) list2.shift();
325
+ byId.set(id, list2);
326
+ }
327
+ function isDocumentEcho(store, id, content) {
328
+ return !!content && !!echoes.get(store)?.get(id)?.includes(content);
329
+ }
330
+ var defaultStore = null;
331
+ function getDefaultDocumentStore() {
332
+ if (!defaultStore) defaultStore = createDocumentStore();
333
+ return defaultStore;
334
+ }
335
+
336
+ // src/utils/update-document-program.ts
337
+ function readString(src, i) {
338
+ if (src[i] !== '"') return null;
339
+ let j = i + 1;
340
+ let value = "";
341
+ while (j < src.length) {
342
+ const ch = src[j];
343
+ if (ch === '"') return { start: i, end: j + 1, value };
344
+ if (ch === "\\") {
345
+ const next = src[j + 1];
346
+ if (next === void 0) return null;
347
+ if (next === "u" && /^[0-9a-fA-F]{4}$/.test(src.slice(j + 2, j + 6))) {
348
+ value += String.fromCharCode(parseInt(src.slice(j + 2, j + 6), 16));
349
+ j += 6;
350
+ continue;
351
+ }
352
+ value += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next === "b" ? "\b" : next === "f" ? "\f" : next;
353
+ j += 2;
354
+ continue;
355
+ }
356
+ value += ch;
357
+ j++;
358
+ }
359
+ return null;
360
+ }
361
+ var skipSpace = (src, i) => {
362
+ while (i < src.length && /\s/.test(src[i])) i++;
363
+ return i;
364
+ };
365
+ var toLangString = (value) => JSON.stringify(value);
366
+ function updateDocumentInProgram(program, documentId, update, componentName = "DocumentEditor") {
367
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
368
+ let match;
369
+ while (match = re.exec(program)) {
370
+ const args = [];
371
+ let i = match.index + match[0].length;
372
+ for (let n = 0; n < 3; n++) {
373
+ i = skipSpace(program, i);
374
+ const tok = readString(program, i);
375
+ if (!tok) break;
376
+ args.push(tok);
377
+ i = skipSpace(program, tok.end);
378
+ if (program[i] === ",") i++;
379
+ else break;
380
+ }
381
+ if (args[0]?.value !== documentId) continue;
382
+ const edits = [];
383
+ if (update.title !== void 0 && args[1] && args[1].value !== update.title) {
384
+ edits.push({ start: args[1].start, end: args[1].end, text: toLangString(update.title) });
385
+ }
386
+ if (update.content !== void 0 && args[2] && args[2].value !== update.content) {
387
+ edits.push({ start: args[2].start, end: args[2].end, text: toLangString(update.content) });
388
+ }
389
+ if (!args[2] && update.content !== void 0) return null;
390
+ let out = program;
391
+ for (const e of edits.sort((a, b) => b.start - a.start)) {
392
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
393
+ }
394
+ return out;
395
+ }
396
+ return null;
397
+ }
398
+ function readDocumentFromProgram(program, documentId, componentName = "DocumentEditor") {
399
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
400
+ let match;
401
+ while (match = re.exec(program)) {
402
+ const args = [];
403
+ let i = match.index + match[0].length;
404
+ for (let n = 0; n < 3; n++) {
405
+ i = skipSpace(program, i);
406
+ const tok = readString(program, i);
407
+ if (!tok) break;
408
+ args.push(tok);
409
+ i = skipSpace(program, tok.end);
410
+ if (program[i] === ",") i++;
411
+ else break;
412
+ }
413
+ if (args[0]?.value === documentId && args[1] && args[2]) return { title: args[1].value, content: args[2].value };
414
+ }
415
+ return null;
416
+ }
417
+
99
418
  // src/core/context.ts
100
419
  import { createContext as createContext2, useContext as useContext2 } from "react";
101
420
  var JuneContext = createContext2({
@@ -654,14 +973,43 @@ function JuneRenderer(props) {
654
973
  onError,
655
974
  onJuneError,
656
975
  documents,
976
+ onResponseChange,
977
+ responseChangeDelay = 300,
657
978
  loadingFallback = null,
658
979
  emptyFallback = null,
659
980
  errorFallback,
660
981
  resetKeys,
661
982
  ...rendererProps
662
983
  } = props;
663
- const latest = useRef({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError });
664
- latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError };
984
+ const latest = useRef({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response });
985
+ latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response };
986
+ const syncTimers = useRef(/* @__PURE__ */ new Map());
987
+ useEffect(() => {
988
+ const timers = syncTimers.current;
989
+ return () => {
990
+ timers.forEach(clearTimeout);
991
+ timers.clear();
992
+ };
993
+ }, []);
994
+ const syncProgram = useCallback(async (change, store) => {
995
+ const l = latest.current;
996
+ if (!l.onResponseChange || typeof l.response !== "string") return;
997
+ const record = store.get(change.documentId);
998
+ const title = record?.title ?? change.title;
999
+ const json = record?.json ?? change.json;
1000
+ try {
1001
+ const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-RKD5SLXR.js");
1002
+ const markdown = docToMarkdown2(json);
1003
+ const program = latest.current.response;
1004
+ if (typeof program !== "string") return;
1005
+ const next = updateDocumentInProgram(program, change.documentId, { title, content: markdown });
1006
+ if (next === null || next === program) return;
1007
+ rememberDocumentEcho(store, change.documentId, markdown);
1008
+ latest.current.onResponseChange?.(next, { documentId: change.documentId, title, markdown, source: change.source });
1009
+ } catch (error) {
1010
+ latest.current.onJuneError?.(toJuneError(error, "DOCUMENT_ERROR", "Updating the program failed"));
1011
+ }
1012
+ }, []);
665
1013
  const handleAction = useCallback((event) => {
666
1014
  const l = latest.current;
667
1015
  l.onAction?.(event);
@@ -700,12 +1048,27 @@ function JuneRenderer(props) {
700
1048
  const documentOptions = useMemo2(
701
1049
  () => ({
702
1050
  ...documents,
1051
+ onDocumentChange: (change) => {
1052
+ documents?.onDocumentChange?.(change);
1053
+ if (!latest.current.onResponseChange || change.source === "ai") return;
1054
+ const store = documents?.store ?? getDefaultDocumentStore();
1055
+ const timers = syncTimers.current;
1056
+ const prev = timers.get(change.documentId);
1057
+ if (prev) clearTimeout(prev);
1058
+ timers.set(
1059
+ change.documentId,
1060
+ setTimeout(() => {
1061
+ timers.delete(change.documentId);
1062
+ void syncProgram(change, store);
1063
+ }, Math.max(0, latest.current.responseChangeDelay))
1064
+ );
1065
+ },
703
1066
  onError: (error) => {
704
1067
  documents?.onError?.(error);
705
1068
  latest.current.onJuneError?.(error);
706
1069
  }
707
1070
  }),
708
- [documents]
1071
+ [documents, syncProgram]
709
1072
  );
710
1073
  const parts = useMemo2(
711
1074
  () => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
@@ -798,7 +1161,7 @@ function defineJuneAction(definition) {
798
1161
  }
799
1162
 
800
1163
  // src/streaming/use-june-stream.ts
801
- import { useCallback as useCallback2, useEffect, useMemo as useMemo3, useRef as useRef2, useState } from "react";
1164
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState } from "react";
802
1165
 
803
1166
  // src/streaming/stream-handler.ts
804
1167
  function isReadableStream(source) {
@@ -1053,7 +1416,7 @@ function useJuneStream(options = {}) {
1053
1416
  clearTimer();
1054
1417
  setState(INITIAL);
1055
1418
  }, [clearTimer]);
1056
- useEffect(
1419
+ useEffect2(
1057
1420
  () => () => {
1058
1421
  runIdRef.current++;
1059
1422
  controllerRef.current?.abort();
@@ -1090,18 +1453,1221 @@ function DocumentDraft({ title, words, state }) {
1090
1453
 
1091
1454
  // src/components/document-editor/document-editor.tsx
1092
1455
  import { useEditor } from "@tiptap/react";
1093
- import { useCallback as useCallback3, useEffect as useEffect2, useId, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
1456
+ import { useCallback as useCallback3, useEffect as useEffect6, useId, useMemo as useMemo4, useRef as useRef7, useState as useState6 } from "react";
1094
1457
 
1095
- // src/components/document-editor/document-content.tsx
1096
- import { EditorContent } from "@tiptap/react";
1097
- import { jsx as jsx5 } from "react/jsx-runtime";
1098
- function DocumentContent({ editor }) {
1099
- return /* @__PURE__ */ jsx5("div", { className: "june-doc__content", children: /* @__PURE__ */ jsx5(EditorContent, { editor }) });
1458
+ // src/components/document-editor/document-bubble.tsx
1459
+ import { useEditorState as useEditorState2 } from "@tiptap/react";
1460
+ import { BubbleMenu } from "@tiptap/react/menus";
1461
+
1462
+ // src/components/document-editor/document-toolbar.tsx
1463
+ import { useEditorState } from "@tiptap/react";
1464
+ import { useRef as useRef4, useState as useState3 } from "react";
1465
+
1466
+ // src/components/document-editor/document-icons.tsx
1467
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1468
+ var PATHS = {
1469
+ undo: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1470
+ /* @__PURE__ */ jsx5("path", { d: "M9 14 4 9l5-5" }),
1471
+ /* @__PURE__ */ jsx5("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
1472
+ ] }),
1473
+ redo: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1474
+ /* @__PURE__ */ jsx5("path", { d: "m15 14 5-5-5-5" }),
1475
+ /* @__PURE__ */ jsx5("path", { d: "M20 9H9.5a5.5 5.5 0 0 0 0 11H13" })
1476
+ ] }),
1477
+ bold: /* @__PURE__ */ jsx5("path", { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" }),
1478
+ italic: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1479
+ /* @__PURE__ */ jsx5("line", { x1: "19", x2: "10", y1: "4", y2: "4" }),
1480
+ /* @__PURE__ */ jsx5("line", { x1: "14", x2: "5", y1: "20", y2: "20" }),
1481
+ /* @__PURE__ */ jsx5("line", { x1: "15", x2: "9", y1: "4", y2: "20" })
1482
+ ] }),
1483
+ underline: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1484
+ /* @__PURE__ */ jsx5("path", { d: "M6 4v6a6 6 0 0 0 12 0V4" }),
1485
+ /* @__PURE__ */ jsx5("line", { x1: "4", x2: "20", y1: "20", y2: "20" })
1486
+ ] }),
1487
+ strike: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1488
+ /* @__PURE__ */ jsx5("path", { d: "M16 4H9a3 3 0 0 0-2.83 4" }),
1489
+ /* @__PURE__ */ jsx5("path", { d: "M14 12a4 4 0 0 1 0 8H6" }),
1490
+ /* @__PURE__ */ jsx5("line", { x1: "4", x2: "20", y1: "12", y2: "12" })
1491
+ ] }),
1492
+ code: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1493
+ /* @__PURE__ */ jsx5("path", { d: "m16 18 6-6-6-6" }),
1494
+ /* @__PURE__ */ jsx5("path", { d: "m8 6-6 6 6 6" })
1495
+ ] }),
1496
+ codeBlock: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1497
+ /* @__PURE__ */ jsx5("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1498
+ /* @__PURE__ */ jsx5("path", { d: "m10 9-3 3 3 3" }),
1499
+ /* @__PURE__ */ jsx5("path", { d: "m14 15 3-3-3-3" })
1500
+ ] }),
1501
+ superscript: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1502
+ /* @__PURE__ */ jsx5("path", { d: "m4 19 8-8" }),
1503
+ /* @__PURE__ */ jsx5("path", { d: "m12 19-8-8" }),
1504
+ /* @__PURE__ */ jsx5("path", { d: "M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07" })
1505
+ ] }),
1506
+ subscript: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1507
+ /* @__PURE__ */ jsx5("path", { d: "m4 5 8 8" }),
1508
+ /* @__PURE__ */ jsx5("path", { d: "m12 5-8 8" }),
1509
+ /* @__PURE__ */ jsx5("path", { d: "M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07" })
1510
+ ] }),
1511
+ alignLeft: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1512
+ /* @__PURE__ */ jsx5("path", { d: "M15 12H3" }),
1513
+ /* @__PURE__ */ jsx5("path", { d: "M17 18H3" }),
1514
+ /* @__PURE__ */ jsx5("path", { d: "M21 6H3" })
1515
+ ] }),
1516
+ alignCenter: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1517
+ /* @__PURE__ */ jsx5("path", { d: "M17 12H7" }),
1518
+ /* @__PURE__ */ jsx5("path", { d: "M19 18H5" }),
1519
+ /* @__PURE__ */ jsx5("path", { d: "M21 6H3" })
1520
+ ] }),
1521
+ alignRight: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1522
+ /* @__PURE__ */ jsx5("path", { d: "M21 12H9" }),
1523
+ /* @__PURE__ */ jsx5("path", { d: "M21 18H7" }),
1524
+ /* @__PURE__ */ jsx5("path", { d: "M21 6H3" })
1525
+ ] }),
1526
+ alignJustify: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1527
+ /* @__PURE__ */ jsx5("path", { d: "M3 12h18" }),
1528
+ /* @__PURE__ */ jsx5("path", { d: "M3 18h18" }),
1529
+ /* @__PURE__ */ jsx5("path", { d: "M3 6h18" })
1530
+ ] }),
1531
+ list: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1532
+ /* @__PURE__ */ jsx5("path", { d: "M3 12h.01" }),
1533
+ /* @__PURE__ */ jsx5("path", { d: "M3 18h.01" }),
1534
+ /* @__PURE__ */ jsx5("path", { d: "M3 6h.01" }),
1535
+ /* @__PURE__ */ jsx5("path", { d: "M8 12h13" }),
1536
+ /* @__PURE__ */ jsx5("path", { d: "M8 18h13" }),
1537
+ /* @__PURE__ */ jsx5("path", { d: "M8 6h13" })
1538
+ ] }),
1539
+ listOrdered: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1540
+ /* @__PURE__ */ jsx5("path", { d: "M10 12h11" }),
1541
+ /* @__PURE__ */ jsx5("path", { d: "M10 18h11" }),
1542
+ /* @__PURE__ */ jsx5("path", { d: "M10 6h11" }),
1543
+ /* @__PURE__ */ jsx5("path", { d: "M4 10h2" }),
1544
+ /* @__PURE__ */ jsx5("path", { d: "M4 6h1v4" }),
1545
+ /* @__PURE__ */ jsx5("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })
1546
+ ] }),
1547
+ outdent: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1548
+ /* @__PURE__ */ jsx5("path", { d: "M21 12H11" }),
1549
+ /* @__PURE__ */ jsx5("path", { d: "M21 18H11" }),
1550
+ /* @__PURE__ */ jsx5("path", { d: "M21 6H11" }),
1551
+ /* @__PURE__ */ jsx5("path", { d: "m7 8-4 4 4 4" })
1552
+ ] }),
1553
+ indent: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1554
+ /* @__PURE__ */ jsx5("path", { d: "M21 12H11" }),
1555
+ /* @__PURE__ */ jsx5("path", { d: "M21 18H11" }),
1556
+ /* @__PURE__ */ jsx5("path", { d: "M21 6H11" }),
1557
+ /* @__PURE__ */ jsx5("path", { d: "m3 8 4 4-4 4" })
1558
+ ] }),
1559
+ quote: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1560
+ /* @__PURE__ */ jsx5("path", { d: "M17 6H3" }),
1561
+ /* @__PURE__ */ jsx5("path", { d: "M21 12H8" }),
1562
+ /* @__PURE__ */ jsx5("path", { d: "M21 18H8" }),
1563
+ /* @__PURE__ */ jsx5("path", { d: "M3 12v6" })
1564
+ ] }),
1565
+ pilcrow: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1566
+ /* @__PURE__ */ jsx5("path", { d: "M13 4v16" }),
1567
+ /* @__PURE__ */ jsx5("path", { d: "M17 4v16" }),
1568
+ /* @__PURE__ */ jsx5("path", { d: "M19 4H9.5a4.5 4.5 0 0 0 0 9H13" })
1569
+ ] }),
1570
+ pageBreak: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1571
+ /* @__PURE__ */ jsx5("line", { x1: "3", x2: "21", y1: "12", y2: "12" }),
1572
+ /* @__PURE__ */ jsx5("polyline", { points: "8 8 12 4 16 8" }),
1573
+ /* @__PURE__ */ jsx5("polyline", { points: "16 16 12 20 8 16" })
1574
+ ] }),
1575
+ divider: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1576
+ /* @__PURE__ */ jsx5("path", { d: "M3 12h18" }),
1577
+ /* @__PURE__ */ jsx5("path", { d: "M8 6h8", opacity: ".45" }),
1578
+ /* @__PURE__ */ jsx5("path", { d: "M8 18h8", opacity: ".45" })
1579
+ ] }),
1580
+ layout: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1581
+ /* @__PURE__ */ jsx5("rect", { width: "18", height: "7", x: "3", y: "3", rx: "1" }),
1582
+ /* @__PURE__ */ jsx5("rect", { width: "9", height: "7", x: "3", y: "14", rx: "1" }),
1583
+ /* @__PURE__ */ jsx5("rect", { width: "5", height: "7", x: "16", y: "14", rx: "1" })
1584
+ ] }),
1585
+ search: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1586
+ /* @__PURE__ */ jsx5("circle", { cx: "11", cy: "11", r: "8" }),
1587
+ /* @__PURE__ */ jsx5("path", { d: "m21 21-4.3-4.3" })
1588
+ ] }),
1589
+ palette: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1590
+ /* @__PURE__ */ jsx5("circle", { cx: "13.5", cy: "6.5", r: ".5", fill: "currentColor" }),
1591
+ /* @__PURE__ */ jsx5("circle", { cx: "17.5", cy: "10.5", r: ".5", fill: "currentColor" }),
1592
+ /* @__PURE__ */ jsx5("circle", { cx: "8.5", cy: "7.5", r: ".5", fill: "currentColor" }),
1593
+ /* @__PURE__ */ jsx5("circle", { cx: "6.5", cy: "12.5", r: ".5", fill: "currentColor" }),
1594
+ /* @__PURE__ */ jsx5("path", { d: "M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.65-.75 1.65-1.69 0-.44-.18-.84-.44-1.13-.29-.29-.44-.65-.44-1.13a1.64 1.64 0 0 1 1.67-1.67h2c3.05 0 5.55-2.5 5.55-5.55C21.97 6.01 17.46 2 12 2z" })
1595
+ ] }),
1596
+ highlighter: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1597
+ /* @__PURE__ */ jsx5("path", { d: "m9 11-6 6v3h9l3-3" }),
1598
+ /* @__PURE__ */ jsx5("path", { d: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" })
1599
+ ] }),
1600
+ link: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1601
+ /* @__PURE__ */ jsx5("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
1602
+ /* @__PURE__ */ jsx5("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
1603
+ ] }),
1604
+ image: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1605
+ /* @__PURE__ */ jsx5("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
1606
+ /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "2" }),
1607
+ /* @__PURE__ */ jsx5("path", { d: "m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21" })
1608
+ ] }),
1609
+ table: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1610
+ /* @__PURE__ */ jsx5("path", { d: "M12 3v18" }),
1611
+ /* @__PURE__ */ jsx5("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
1612
+ /* @__PURE__ */ jsx5("path", { d: "M3 9h18" }),
1613
+ /* @__PURE__ */ jsx5("path", { d: "M3 15h18" })
1614
+ ] }),
1615
+ eraser: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1616
+ /* @__PURE__ */ jsx5("path", { d: "m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21" }),
1617
+ /* @__PURE__ */ jsx5("path", { d: "M22 21H7" }),
1618
+ /* @__PURE__ */ jsx5("path", { d: "m5 11 9 9" })
1619
+ ] }),
1620
+ save: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1621
+ /* @__PURE__ */ jsx5("path", { d: "M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" }),
1622
+ /* @__PURE__ */ jsx5("path", { d: "M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" }),
1623
+ /* @__PURE__ */ jsx5("path", { d: "M7 3v4a1 1 0 0 0 1 1h7" })
1624
+ ] }),
1625
+ download: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1626
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1627
+ /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
1628
+ /* @__PURE__ */ jsx5("line", { x1: "12", x2: "12", y1: "15", y2: "3" })
1629
+ ] }),
1630
+ chevronDown: /* @__PURE__ */ jsx5("path", { d: "m6 9 6 6 6-6" }),
1631
+ chevronUp: /* @__PURE__ */ jsx5("path", { d: "m18 15-6-6-6 6" }),
1632
+ close: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1633
+ /* @__PURE__ */ jsx5("path", { d: "M18 6 6 18" }),
1634
+ /* @__PURE__ */ jsx5("path", { d: "m6 6 12 12" })
1635
+ ] }),
1636
+ maximize: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1637
+ /* @__PURE__ */ jsx5("path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
1638
+ /* @__PURE__ */ jsx5("path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }),
1639
+ /* @__PURE__ */ jsx5("path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }),
1640
+ /* @__PURE__ */ jsx5("path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" })
1641
+ ] }),
1642
+ copy: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1643
+ /* @__PURE__ */ jsx5("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2" }),
1644
+ /* @__PURE__ */ jsx5("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
1645
+ ] }),
1646
+ file: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1647
+ /* @__PURE__ */ jsx5("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
1648
+ /* @__PURE__ */ jsx5("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
1649
+ ] }),
1650
+ plus: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1651
+ /* @__PURE__ */ jsx5("path", { d: "M5 12h14" }),
1652
+ /* @__PURE__ */ jsx5("path", { d: "M12 5v14" })
1653
+ ] }),
1654
+ minus: /* @__PURE__ */ jsx5("path", { d: "M5 12h14" }),
1655
+ check: /* @__PURE__ */ jsx5("path", { d: "M20 6 9 17l-5-5" }),
1656
+ sparkles: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1657
+ /* @__PURE__ */ jsx5("path", { d: "M9.94 14.06 8 20l-1.94-5.94L0 12l6.06-1.94L8 4l1.94 6.06L16 12z", transform: "translate(3 -1) scale(.85)" }),
1658
+ /* @__PURE__ */ jsx5("path", { d: "M20 3v4" }),
1659
+ /* @__PURE__ */ jsx5("path", { d: "M22 5h-4" })
1660
+ ] }),
1661
+ upload: /* @__PURE__ */ jsxs3(Fragment2, { children: [
1662
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1663
+ /* @__PURE__ */ jsx5("polyline", { points: "17 8 12 3 7 8" }),
1664
+ /* @__PURE__ */ jsx5("line", { x1: "12", x2: "12", y1: "3", y2: "15" })
1665
+ ] })
1666
+ };
1667
+ function Icon({ name, size = 16 }) {
1668
+ return /* @__PURE__ */ jsx5(
1669
+ "svg",
1670
+ {
1671
+ className: "june-doc__icon",
1672
+ width: size,
1673
+ height: size,
1674
+ viewBox: "0 0 24 24",
1675
+ fill: "none",
1676
+ stroke: "currentColor",
1677
+ strokeWidth: 2,
1678
+ strokeLinecap: "round",
1679
+ strokeLinejoin: "round",
1680
+ "aria-hidden": "true",
1681
+ focusable: "false",
1682
+ children: PATHS[name]
1683
+ }
1684
+ );
1685
+ }
1686
+
1687
+ // src/components/document-editor/document-popover.tsx
1688
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
1689
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1690
+ function Dropdown({ trigger, children, align = "start", className, onOpen }) {
1691
+ const [open, setOpen] = useState2(false);
1692
+ const anchor = useRef3(null);
1693
+ useEffect3(() => {
1694
+ if (!open) return;
1695
+ const onDown = (e) => {
1696
+ if (anchor.current && !anchor.current.contains(e.target)) setOpen(false);
1697
+ };
1698
+ document.addEventListener("mousedown", onDown);
1699
+ return () => document.removeEventListener("mousedown", onDown);
1700
+ }, [open]);
1701
+ const toggle = () => setOpen((v) => {
1702
+ if (!v) onOpen?.();
1703
+ return !v;
1704
+ });
1705
+ const close = () => setOpen(false);
1706
+ return /* @__PURE__ */ jsxs4(
1707
+ "div",
1708
+ {
1709
+ ref: anchor,
1710
+ className: "june-doc__anchor",
1711
+ onKeyDown: (e) => {
1712
+ if (e.key === "Escape" && open) {
1713
+ e.stopPropagation();
1714
+ close();
1715
+ }
1716
+ },
1717
+ children: [
1718
+ trigger({ open, toggle }),
1719
+ open ? /* @__PURE__ */ jsx6("div", { className: `june-doc__pop june-doc__pop--${align}${className ? ` ${className}` : ""}`, children: children(close) }) : null
1720
+ ]
1721
+ }
1722
+ );
1723
+ }
1724
+
1725
+ // src/components/document-editor/document-toolbar.tsx
1726
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1727
+ var FONT_FAMILIES = [
1728
+ { label: "Default", value: "" },
1729
+ { label: "Arial", value: "Arial, Helvetica, sans-serif" },
1730
+ { label: "Calibri", value: "Calibri, Carlito, sans-serif" },
1731
+ { label: "Georgia", value: "Georgia, serif" },
1732
+ { label: "Garamond", value: "Garamond, 'EB Garamond', serif" },
1733
+ { label: "Times New Roman", value: "'Times New Roman', Times, serif" },
1734
+ { label: "Verdana", value: "Verdana, Geneva, sans-serif" },
1735
+ { label: "Trebuchet MS", value: "'Trebuchet MS', sans-serif" },
1736
+ { label: "Courier New", value: "'Courier New', Courier, monospace" }
1737
+ ];
1738
+ var FONT_SIZES = [8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 48, 60, 72];
1739
+ var TEXT_COLORS = [
1740
+ "#111827",
1741
+ "#374151",
1742
+ "#6b7280",
1743
+ "#9ca3af",
1744
+ "#dc2626",
1745
+ "#ea580c",
1746
+ "#ca8a04",
1747
+ "#16a34a",
1748
+ "#0d9488",
1749
+ "#0284c7",
1750
+ "#2563eb",
1751
+ "#4f46e5",
1752
+ "#9333ea",
1753
+ "#db2777",
1754
+ "#7f1d1d",
1755
+ "#1e3a8a"
1756
+ ];
1757
+ var HIGHLIGHT_COLORS = [
1758
+ "#fef08a",
1759
+ "#fde68a",
1760
+ "#fed7aa",
1761
+ "#fecaca",
1762
+ "#fbcfe8",
1763
+ "#e9d5ff",
1764
+ "#c7d2fe",
1765
+ "#bfdbfe",
1766
+ "#a5f3fc",
1767
+ "#bbf7d0",
1768
+ "#d9f99d",
1769
+ "#e5e7eb"
1770
+ ];
1771
+ var MAX_IMAGE_BYTES = 8 * 1024 * 1024;
1772
+ function Btn({ label, icon, active, disabled, toggle, onClick, children, className, text }) {
1773
+ return /* @__PURE__ */ jsxs5(
1774
+ "button",
1775
+ {
1776
+ type: "button",
1777
+ className: `june-doc__btn${icon && !text && !children ? " june-doc__btn--icon" : ""}${className ? ` ${className}` : ""}`,
1778
+ "aria-label": label,
1779
+ title: label,
1780
+ "aria-pressed": toggle ? !!active : void 0,
1781
+ "data-active": active || void 0,
1782
+ disabled,
1783
+ onMouseDown: (e) => e.preventDefault(),
1784
+ onClick,
1785
+ children: [
1786
+ icon ? /* @__PURE__ */ jsx7(Icon, { name: icon }) : null,
1787
+ children ?? (text ? /* @__PURE__ */ jsx7("span", { children: label }) : null)
1788
+ ]
1789
+ }
1790
+ );
1791
+ }
1792
+ function Group({ label, children }) {
1793
+ return /* @__PURE__ */ jsxs5("div", { className: "june-doc__group", role: "group", "aria-label": label, children: [
1794
+ label ? /* @__PURE__ */ jsx7("span", { className: "june-doc__group-label", "aria-hidden": "true", children: label }) : null,
1795
+ children
1796
+ ] });
1797
+ }
1798
+ function Field({ label, children }) {
1799
+ return /* @__PURE__ */ jsxs5("label", { className: "june-doc__field", children: [
1800
+ /* @__PURE__ */ jsx7("span", { className: "june-doc__field-label", children: label }),
1801
+ children
1802
+ ] });
1803
+ }
1804
+ function ColorPicker({ label, icon, colors, current, disabled, resetLabel, onPick, onReset }) {
1805
+ return /* @__PURE__ */ jsx7(
1806
+ Dropdown,
1807
+ {
1808
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsxs5(
1809
+ "button",
1810
+ {
1811
+ type: "button",
1812
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--color",
1813
+ "aria-label": label,
1814
+ title: label,
1815
+ "aria-expanded": open,
1816
+ "aria-haspopup": "dialog",
1817
+ disabled,
1818
+ onMouseDown: (e) => e.preventDefault(),
1819
+ onClick: toggle,
1820
+ children: [
1821
+ /* @__PURE__ */ jsx7(Icon, { name: icon }),
1822
+ /* @__PURE__ */ jsx7("span", { className: "june-doc__color-bar", style: { background: current ?? "transparent" } })
1823
+ ]
1824
+ }
1825
+ ),
1826
+ children: (close) => /* @__PURE__ */ jsxs5("div", { className: "june-doc__swatches-wrap", role: "dialog", "aria-label": label, children: [
1827
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__pop-title", children: label }),
1828
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__swatches", children: colors.map((c) => /* @__PURE__ */ jsx7(
1829
+ "button",
1830
+ {
1831
+ type: "button",
1832
+ className: "june-doc__swatch",
1833
+ style: { background: c },
1834
+ "aria-label": c,
1835
+ title: c,
1836
+ "data-active": current?.toLowerCase() === c ? true : void 0,
1837
+ onMouseDown: (e) => e.preventDefault(),
1838
+ onClick: () => {
1839
+ onPick(c);
1840
+ close();
1841
+ }
1842
+ },
1843
+ c
1844
+ )) }),
1845
+ /* @__PURE__ */ jsxs5("div", { className: "june-doc__pop-row", children: [
1846
+ /* @__PURE__ */ jsx7(
1847
+ "button",
1848
+ {
1849
+ type: "button",
1850
+ className: "june-doc__btn june-doc__btn--ghost",
1851
+ onMouseDown: (e) => e.preventDefault(),
1852
+ onClick: () => {
1853
+ onReset();
1854
+ close();
1855
+ },
1856
+ children: resetLabel
1857
+ }
1858
+ ),
1859
+ /* @__PURE__ */ jsxs5("label", { className: "june-doc__custom-color", title: "Custom color", children: [
1860
+ /* @__PURE__ */ jsx7("span", { children: "Custom" }),
1861
+ /* @__PURE__ */ jsx7(
1862
+ "input",
1863
+ {
1864
+ type: "color",
1865
+ "aria-label": `${label}: custom`,
1866
+ defaultValue: current && /^#[0-9a-f]{6}$/i.test(current) ? current : "#000000",
1867
+ onChange: (e) => onPick(e.target.value)
1868
+ }
1869
+ )
1870
+ ] })
1871
+ ] })
1872
+ ] })
1873
+ }
1874
+ );
1875
+ }
1876
+ function LinkPanel({
1877
+ editor,
1878
+ href,
1879
+ isLink,
1880
+ close
1881
+ }) {
1882
+ const [value, setValue] = useState3(href);
1883
+ const apply = () => {
1884
+ const safe = sanitizeHref(value);
1885
+ if (safe) editor.chain().focus().extendMarkRange("link").setLink({ href: safe }).run();
1886
+ close();
1887
+ };
1888
+ const remove = () => {
1889
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
1890
+ close();
1891
+ };
1892
+ return /* @__PURE__ */ jsxs5("div", { className: "june-doc__form", role: "dialog", "aria-label": "Edit link", children: [
1893
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__pop-title", children: "Link" }),
1894
+ /* @__PURE__ */ jsx7(
1895
+ "input",
1896
+ {
1897
+ "aria-label": "Link address",
1898
+ className: "june-doc__input",
1899
+ placeholder: "https://example.com",
1900
+ value,
1901
+ autoFocus: true,
1902
+ onChange: (e) => setValue(e.target.value),
1903
+ onKeyDown: (e) => {
1904
+ if (e.key === "Enter") {
1905
+ e.preventDefault();
1906
+ apply();
1907
+ }
1908
+ }
1909
+ }
1910
+ ),
1911
+ /* @__PURE__ */ jsxs5("div", { className: "june-doc__pop-row june-doc__pop-row--end", children: [
1912
+ isLink ? /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn june-doc__btn--ghost", onClick: remove, children: "Remove" }) : null,
1913
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: close, children: "Cancel" }),
1914
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn june-doc__btn--primary", onClick: apply, children: "Apply" })
1915
+ ] })
1916
+ ] });
1917
+ }
1918
+ function ImagePanel({ editor, close, onNotice }) {
1919
+ const [url, setUrl] = useState3("");
1920
+ const [alt, setAlt] = useState3("");
1921
+ const file = useRef4(null);
1922
+ const insert = (src) => {
1923
+ editor.chain().focus().setImage({ src, alt: alt || void 0 }).run();
1924
+ close();
1925
+ };
1926
+ const fromUrl = () => {
1927
+ const safe = sanitizeHref(url);
1928
+ if (!safe || !/^https?:/i.test(safe)) {
1929
+ onNotice("Enter an http(s) image address");
1930
+ return;
1931
+ }
1932
+ insert(safe);
1933
+ };
1934
+ const fromFile = (f) => {
1935
+ if (!f) return;
1936
+ if (!f.type.startsWith("image/")) return onNotice("That file is not an image");
1937
+ if (f.size > MAX_IMAGE_BYTES) return onNotice("Images must be 8 MB or smaller");
1938
+ const reader = new FileReader();
1939
+ reader.onload = () => typeof reader.result === "string" && insert(reader.result);
1940
+ reader.onerror = () => onNotice("The image could not be read");
1941
+ reader.readAsDataURL(f);
1942
+ };
1943
+ return /* @__PURE__ */ jsxs5("div", { className: "june-doc__form", role: "dialog", "aria-label": "Insert image", children: [
1944
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__pop-title", children: "Image" }),
1945
+ /* @__PURE__ */ jsxs5(
1946
+ "button",
1947
+ {
1948
+ type: "button",
1949
+ className: "june-doc__dropzone",
1950
+ onClick: () => file.current?.click(),
1951
+ onDragOver: (e) => e.preventDefault(),
1952
+ onDrop: (e) => {
1953
+ e.preventDefault();
1954
+ fromFile(e.dataTransfer.files[0]);
1955
+ },
1956
+ children: [
1957
+ /* @__PURE__ */ jsx7(Icon, { name: "upload", size: 18 }),
1958
+ /* @__PURE__ */ jsx7("span", { children: "Upload or drop an image" })
1959
+ ]
1960
+ }
1961
+ ),
1962
+ /* @__PURE__ */ jsx7(
1963
+ "input",
1964
+ {
1965
+ ref: file,
1966
+ type: "file",
1967
+ accept: "image/*",
1968
+ hidden: true,
1969
+ onChange: (e) => fromFile(e.target.files?.[0])
1970
+ }
1971
+ ),
1972
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__or", children: "or" }),
1973
+ /* @__PURE__ */ jsx7(
1974
+ "input",
1975
+ {
1976
+ "aria-label": "Image address",
1977
+ className: "june-doc__input",
1978
+ placeholder: "https://example.com/image.png",
1979
+ value: url,
1980
+ onChange: (e) => setUrl(e.target.value),
1981
+ onKeyDown: (e) => e.key === "Enter" && fromUrl()
1982
+ }
1983
+ ),
1984
+ /* @__PURE__ */ jsx7(
1985
+ "input",
1986
+ {
1987
+ "aria-label": "Alternative text",
1988
+ className: "june-doc__input",
1989
+ placeholder: "Alt text (optional)",
1990
+ value: alt,
1991
+ onChange: (e) => setAlt(e.target.value)
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsxs5("div", { className: "june-doc__pop-row june-doc__pop-row--end", children: [
1995
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: close, children: "Cancel" }),
1996
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn june-doc__btn--primary", disabled: !url.trim(), onClick: fromUrl, children: "Insert" })
1997
+ ] })
1998
+ ] });
1999
+ }
2000
+ function TablePanel({ editor, close }) {
2001
+ const [hover, setHover] = useState3([0, 0]);
2002
+ const N = 8;
2003
+ return /* @__PURE__ */ jsxs5("div", { className: "june-doc__form", role: "dialog", "aria-label": "Insert table", children: [
2004
+ /* @__PURE__ */ jsxs5("div", { className: "june-doc__pop-title", children: [
2005
+ "Table ",
2006
+ hover[0] ? `${hover[0]} \xD7 ${hover[1]}` : ""
2007
+ ] }),
2008
+ /* @__PURE__ */ jsx7("div", { className: "june-doc__grid", onMouseLeave: () => setHover([0, 0]), children: Array.from({ length: N * N }, (_, i) => {
2009
+ const r = Math.floor(i / N) + 1;
2010
+ const c = i % N + 1;
2011
+ return /* @__PURE__ */ jsx7(
2012
+ "button",
2013
+ {
2014
+ type: "button",
2015
+ className: "june-doc__cell",
2016
+ "aria-label": `${r} by ${c} table`,
2017
+ "data-active": r <= hover[0] && c <= hover[1] ? true : void 0,
2018
+ onMouseEnter: () => setHover([r, c]),
2019
+ onFocus: () => setHover([r, c]),
2020
+ onMouseDown: (e) => e.preventDefault(),
2021
+ onClick: () => {
2022
+ editor.chain().focus().insertTable({ rows: r, cols: c, withHeaderRow: true }).run();
2023
+ close();
2024
+ }
2025
+ },
2026
+ i
2027
+ );
2028
+ }) })
2029
+ ] });
2030
+ }
2031
+ var px = (v) => typeof v === "string" ? v.replace(/px$/, "") : "";
2032
+ function DocumentToolbar(props) {
2033
+ const { editor, editable, expanded, onToggleExpanded, showMarks, onToggleMarks, layout, onToggleLayout, findOpen, onToggleFind, onNotice } = props;
2034
+ const s = useEditorState({
2035
+ editor,
2036
+ selector: ({ editor: ed }) => {
2037
+ if (!ed) return null;
2038
+ const ts = ed.getAttributes("textStyle");
2039
+ return {
2040
+ bold: ed.isActive("bold"),
2041
+ italic: ed.isActive("italic"),
2042
+ underline: ed.isActive("underline"),
2043
+ strike: ed.isActive("strike"),
2044
+ code: ed.isActive("code"),
2045
+ sup: ed.isActive("superscript"),
2046
+ sub: ed.isActive("subscript"),
2047
+ bullet: ed.isActive("bulletList"),
2048
+ ordered: ed.isActive("orderedList"),
2049
+ quote: ed.isActive("blockquote"),
2050
+ codeBlock: ed.isActive("codeBlock"),
2051
+ link: ed.isActive("link"),
2052
+ linkHref: ed.getAttributes("link").href ?? "",
2053
+ h1: ed.isActive("heading", { level: 1 }),
2054
+ h2: ed.isActive("heading", { level: 2 }),
2055
+ h3: ed.isActive("heading", { level: 3 }),
2056
+ left: ed.isActive({ textAlign: "left" }),
2057
+ center: ed.isActive({ textAlign: "center" }),
2058
+ right: ed.isActive({ textAlign: "right" }),
2059
+ justify: ed.isActive({ textAlign: "justify" }),
2060
+ inTable: ed.isActive("table"),
2061
+ color: ts.color ?? null,
2062
+ fontFamily: ts.fontFamily ?? "",
2063
+ fontSize: px(ts.fontSize),
2064
+ highlight: ed.isActive("highlight") ? ed.getAttributes("highlight").color ?? "#fef08a" : null,
2065
+ canUndo: ed.can().undo(),
2066
+ canRedo: ed.can().redo(),
2067
+ canSink: ed.can().sinkListItem("listItem"),
2068
+ canLift: ed.can().liftListItem("listItem"),
2069
+ canMerge: ed.can().mergeCells(),
2070
+ canSplit: ed.can().splitCell()
2071
+ };
2072
+ }
2073
+ });
2074
+ const run = () => editor?.chain().focus();
2075
+ const off = !editor || !editable;
2076
+ const style = s?.codeBlock ? "code" : s?.h1 ? "1" : s?.h2 ? "2" : s?.h3 ? "3" : s?.quote ? "quote" : "p";
2077
+ const setStyle = (v) => {
2078
+ if (!editor || !s) return;
2079
+ let c = editor.chain().focus();
2080
+ if (v === "quote") {
2081
+ if (!s.quote) c = c.setParagraph().toggleBlockquote();
2082
+ } else if (v === "code") {
2083
+ if (!s.codeBlock) c = c.toggleCodeBlock();
2084
+ } else {
2085
+ if (s.quote) c = c.lift("blockquote");
2086
+ c = v === "p" ? c.setParagraph() : c.setHeading({ level: Number(v) });
2087
+ }
2088
+ c.run();
2089
+ };
2090
+ const fontKnown = FONT_FAMILIES.some((f) => f.value === s?.fontFamily);
2091
+ const sizes = s?.fontSize && !FONT_SIZES.includes(Number(s.fontSize)) ? [...FONT_SIZES, Number(s.fontSize)].sort((a, b) => a - b) : FONT_SIZES;
2092
+ if (!editable) return null;
2093
+ return /* @__PURE__ */ jsxs5("div", { className: "june-doc__ribbon", role: "toolbar", "aria-label": "Document formatting", children: [
2094
+ /* @__PURE__ */ jsxs5("div", { className: "june-doc__row", children: [
2095
+ /* @__PURE__ */ jsxs5(Group, { label: "History", children: [
2096
+ /* @__PURE__ */ jsx7(Btn, { label: "Undo", icon: "undo", disabled: off || !s?.canUndo, onClick: () => run()?.undo().run() }),
2097
+ /* @__PURE__ */ jsx7(Btn, { label: "Redo", icon: "redo", disabled: off || !s?.canRedo, onClick: () => run()?.redo().run() })
2098
+ ] }),
2099
+ /* @__PURE__ */ jsxs5(Group, { label: "Format", children: [
2100
+ /* @__PURE__ */ jsx7(Btn, { label: "Bold", icon: "bold", toggle: true, active: s?.bold, disabled: off, onClick: () => run()?.toggleBold().run() }),
2101
+ /* @__PURE__ */ jsx7(Btn, { label: "Italic", icon: "italic", toggle: true, active: s?.italic, disabled: off, onClick: () => run()?.toggleItalic().run() }),
2102
+ /* @__PURE__ */ jsx7(Btn, { label: "Underline", icon: "underline", toggle: true, active: s?.underline, disabled: off, onClick: () => run()?.toggleUnderline().run() }),
2103
+ /* @__PURE__ */ jsx7(Btn, { label: "Strikethrough", icon: "strike", toggle: true, active: s?.strike, disabled: off, onClick: () => run()?.toggleStrike().run() }),
2104
+ /* @__PURE__ */ jsx7(Btn, { label: "Inline code", icon: "code", toggle: true, active: s?.code, disabled: off, onClick: () => run()?.toggleCode().run() }),
2105
+ /* @__PURE__ */ jsx7(Btn, { label: "Superscript", icon: "superscript", toggle: true, active: s?.sup, disabled: off, onClick: () => run()?.toggleSuperscript().run() }),
2106
+ /* @__PURE__ */ jsx7(Btn, { label: "Subscript", icon: "subscript", toggle: true, active: s?.sub, disabled: off, onClick: () => run()?.toggleSubscript().run() })
2107
+ ] }),
2108
+ /* @__PURE__ */ jsxs5(Group, { label: "Align", children: [
2109
+ /* @__PURE__ */ jsx7(Btn, { label: "Align left", icon: "alignLeft", toggle: true, active: s?.left, disabled: off, onClick: () => run()?.setTextAlign("left").run() }),
2110
+ /* @__PURE__ */ jsx7(Btn, { label: "Align center", icon: "alignCenter", toggle: true, active: s?.center, disabled: off, onClick: () => run()?.setTextAlign("center").run() }),
2111
+ /* @__PURE__ */ jsx7(Btn, { label: "Align right", icon: "alignRight", toggle: true, active: s?.right, disabled: off, onClick: () => run()?.setTextAlign("right").run() }),
2112
+ /* @__PURE__ */ jsx7(Btn, { label: "Justify", icon: "alignJustify", toggle: true, active: s?.justify, disabled: off, onClick: () => run()?.setTextAlign("justify").run() })
2113
+ ] }),
2114
+ /* @__PURE__ */ jsxs5(Group, { label: "Lists", children: [
2115
+ /* @__PURE__ */ jsx7(Btn, { label: "Bullet list", icon: "list", toggle: true, active: s?.bullet, disabled: off, onClick: () => run()?.toggleBulletList().run() }),
2116
+ /* @__PURE__ */ jsx7(Btn, { label: "Numbered list", icon: "listOrdered", toggle: true, active: s?.ordered, disabled: off, onClick: () => run()?.toggleOrderedList().run() }),
2117
+ /* @__PURE__ */ jsx7(Btn, { label: "Decrease indent", icon: "outdent", disabled: off || !s?.canLift, onClick: () => run()?.liftListItem("listItem").run() }),
2118
+ /* @__PURE__ */ jsx7(Btn, { label: "Increase indent", icon: "indent", disabled: off || !s?.canSink, onClick: () => run()?.sinkListItem("listItem").run() })
2119
+ ] }),
2120
+ /* @__PURE__ */ jsxs5(
2121
+ "button",
2122
+ {
2123
+ type: "button",
2124
+ className: "june-doc__btn june-doc__more",
2125
+ "aria-expanded": expanded,
2126
+ "aria-label": expanded ? "Show fewer tools" : "Show more tools",
2127
+ onMouseDown: (e) => e.preventDefault(),
2128
+ onClick: onToggleExpanded,
2129
+ children: [
2130
+ expanded ? "Less" : "More",
2131
+ /* @__PURE__ */ jsx7(Icon, { name: expanded ? "chevronUp" : "chevronDown", size: 14 })
2132
+ ]
2133
+ }
2134
+ )
2135
+ ] }),
2136
+ expanded ? /* @__PURE__ */ jsxs5("div", { className: "june-doc__row june-doc__row--secondary", children: [
2137
+ /* @__PURE__ */ jsxs5(Group, { label: "Style", children: [
2138
+ /* @__PURE__ */ jsx7(Field, { label: "Style", children: /* @__PURE__ */ jsxs5("select", { className: "june-doc__select", "aria-label": "Text style", disabled: off, value: style, onChange: (e) => setStyle(e.target.value), children: [
2139
+ /* @__PURE__ */ jsx7("option", { value: "p", children: "Paragraph" }),
2140
+ /* @__PURE__ */ jsx7("option", { value: "1", children: "Heading 1" }),
2141
+ /* @__PURE__ */ jsx7("option", { value: "2", children: "Heading 2" }),
2142
+ /* @__PURE__ */ jsx7("option", { value: "3", children: "Heading 3" }),
2143
+ /* @__PURE__ */ jsx7("option", { value: "quote", children: "Quote" }),
2144
+ /* @__PURE__ */ jsx7("option", { value: "code", children: "Code block" })
2145
+ ] }) }),
2146
+ /* @__PURE__ */ jsx7(Field, { label: "Font", children: /* @__PURE__ */ jsxs5(
2147
+ "select",
2148
+ {
2149
+ className: "june-doc__select june-doc__select--font",
2150
+ "aria-label": "Font family",
2151
+ disabled: off,
2152
+ value: s?.fontFamily ?? "",
2153
+ onChange: (e) => {
2154
+ const v = e.target.value;
2155
+ if (v) run()?.setFontFamily(v).run();
2156
+ else run()?.unsetFontFamily().run();
2157
+ },
2158
+ children: [
2159
+ FONT_FAMILIES.map((f) => /* @__PURE__ */ jsx7("option", { value: f.value, style: { fontFamily: f.value || void 0 }, children: f.label }, f.label)),
2160
+ !fontKnown && s?.fontFamily ? /* @__PURE__ */ jsx7("option", { value: s.fontFamily, children: s.fontFamily.split(",")[0].replace(/['"]/g, "") }) : null
2161
+ ]
2162
+ }
2163
+ ) }),
2164
+ /* @__PURE__ */ jsx7(Field, { label: "Size", children: /* @__PURE__ */ jsxs5(
2165
+ "select",
2166
+ {
2167
+ className: "june-doc__select june-doc__select--size",
2168
+ "aria-label": "Font size",
2169
+ disabled: off,
2170
+ value: s?.fontSize ?? "",
2171
+ onChange: (e) => {
2172
+ const v = e.target.value;
2173
+ if (v) run()?.setFontSize(`${v}px`).run();
2174
+ else run()?.unsetFontSize().run();
2175
+ },
2176
+ children: [
2177
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Auto" }),
2178
+ sizes.map((n) => /* @__PURE__ */ jsx7("option", { value: String(n), children: n }, n))
2179
+ ]
2180
+ }
2181
+ ) })
2182
+ ] }),
2183
+ /* @__PURE__ */ jsx7(Btn, { label: "Show formatting marks", icon: "pilcrow", toggle: true, active: showMarks, onClick: onToggleMarks, text: true, className: "june-doc__btn--marks", children: /* @__PURE__ */ jsx7("span", { children: "Paragraph" }) }),
2184
+ /* @__PURE__ */ jsxs5(Group, { label: "Document", children: [
2185
+ /* @__PURE__ */ jsx7(Btn, { label: "Insert page break", icon: "pageBreak", disabled: off, onClick: () => run()?.setPageBreak().run() }),
2186
+ /* @__PURE__ */ jsx7(Btn, { label: "Insert divider", icon: "divider", disabled: off, onClick: () => run()?.setHorizontalRule().run() }),
2187
+ /* @__PURE__ */ jsx7(Btn, { label: layout === "page" ? "Switch to full-width layout" : "Switch to page layout", icon: "layout", toggle: true, active: layout === "fluid", onClick: onToggleLayout }),
2188
+ /* @__PURE__ */ jsx7(Btn, { label: "Find and replace", icon: "search", toggle: true, active: findOpen, onClick: onToggleFind })
2189
+ ] }),
2190
+ /* @__PURE__ */ jsxs5(Group, { label: "Color", children: [
2191
+ /* @__PURE__ */ jsx7(
2192
+ ColorPicker,
2193
+ {
2194
+ label: "Text color",
2195
+ icon: "palette",
2196
+ colors: TEXT_COLORS,
2197
+ current: s?.color ?? null,
2198
+ disabled: off,
2199
+ resetLabel: "Default color",
2200
+ onPick: (c) => run()?.setColor(c).run(),
2201
+ onReset: () => run()?.unsetColor().run()
2202
+ }
2203
+ ),
2204
+ /* @__PURE__ */ jsx7(
2205
+ ColorPicker,
2206
+ {
2207
+ label: "Highlight",
2208
+ icon: "highlighter",
2209
+ colors: HIGHLIGHT_COLORS,
2210
+ current: s?.highlight ?? null,
2211
+ disabled: off,
2212
+ resetLabel: "No highlight",
2213
+ onPick: (c) => run()?.setHighlight({ color: c }).run(),
2214
+ onReset: () => run()?.unsetHighlight().run()
2215
+ }
2216
+ )
2217
+ ] }),
2218
+ /* @__PURE__ */ jsxs5(Group, { label: "Insert", children: [
2219
+ editor ? /* @__PURE__ */ jsx7(
2220
+ Dropdown,
2221
+ {
2222
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsx7(Btn, { label: "Link", icon: "link", toggle: true, active: s?.link || open, disabled: off, onClick: toggle }),
2223
+ children: (close) => /* @__PURE__ */ jsx7(LinkPanel, { editor, href: s?.linkHref ?? "", isLink: !!s?.link, close })
2224
+ }
2225
+ ) : null,
2226
+ editor ? /* @__PURE__ */ jsx7(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ jsx7(Btn, { label: "Insert image", icon: "image", active: open, disabled: off, onClick: toggle }), children: (close) => /* @__PURE__ */ jsx7(ImagePanel, { editor, close, onNotice }) }) : null,
2227
+ editor ? /* @__PURE__ */ jsx7(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ jsx7(Btn, { label: "Insert table", icon: "table", active: open, disabled: off, onClick: toggle }), children: (close) => /* @__PURE__ */ jsx7(TablePanel, { editor, close }) }) : null
2228
+ ] }),
2229
+ /* @__PURE__ */ jsx7(Group, { label: "Actions", children: /* @__PURE__ */ jsx7(Btn, { label: "Clear formatting", icon: "eraser", disabled: off, onClick: () => run()?.unsetAllMarks().clearNodes().run() }) })
2230
+ ] }) : null,
2231
+ s?.inTable ? /* @__PURE__ */ jsx7("div", { className: "june-doc__row june-doc__row--context", children: /* @__PURE__ */ jsxs5(Group, { label: "Table", children: [
2232
+ /* @__PURE__ */ jsx7(Btn, { label: "Add row below", disabled: off, onClick: () => run()?.addRowAfter().run(), children: "+ Row" }),
2233
+ /* @__PURE__ */ jsx7(Btn, { label: "Add column right", disabled: off, onClick: () => run()?.addColumnAfter().run(), children: "+ Column" }),
2234
+ /* @__PURE__ */ jsx7(Btn, { label: "Delete row", disabled: off, onClick: () => run()?.deleteRow().run(), children: "\u2212 Row" }),
2235
+ /* @__PURE__ */ jsx7(Btn, { label: "Delete column", disabled: off, onClick: () => run()?.deleteColumn().run(), children: "\u2212 Column" }),
2236
+ /* @__PURE__ */ jsx7(Btn, { label: "Merge cells", disabled: off || !s?.canMerge, onClick: () => run()?.mergeCells().run(), children: "Merge" }),
2237
+ /* @__PURE__ */ jsx7(Btn, { label: "Split cell", disabled: off || !s?.canSplit, onClick: () => run()?.splitCell().run(), children: "Split" }),
2238
+ /* @__PURE__ */ jsx7(Btn, { label: "Toggle header row", disabled: off, onClick: () => run()?.toggleHeaderRow().run(), children: "Header row" }),
2239
+ /* @__PURE__ */ jsx7(Btn, { label: "Delete table", disabled: off, onClick: () => run()?.deleteTable().run(), className: "june-doc__btn--danger", children: "Delete table" })
2240
+ ] }) }) : null
2241
+ ] });
2242
+ }
2243
+
2244
+ // src/components/document-editor/document-bubble.tsx
2245
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2246
+ function DocumentBubbleMenu({ editor }) {
2247
+ const s = useEditorState2({
2248
+ editor,
2249
+ selector: ({ editor: ed }) => ({
2250
+ bold: ed.isActive("bold"),
2251
+ italic: ed.isActive("italic"),
2252
+ underline: ed.isActive("underline"),
2253
+ strike: ed.isActive("strike"),
2254
+ link: ed.isActive("link"),
2255
+ linkHref: ed.getAttributes("link").href ?? "",
2256
+ color: ed.getAttributes("textStyle").color ?? null,
2257
+ highlight: ed.isActive("highlight") ? ed.getAttributes("highlight").color ?? "#fef08a" : null
2258
+ })
2259
+ });
2260
+ const run = () => editor.chain().focus();
2261
+ return /* @__PURE__ */ jsxs6(
2262
+ BubbleMenu,
2263
+ {
2264
+ editor,
2265
+ className: "june-doc__bubble",
2266
+ options: { placement: "top", offset: 8 },
2267
+ shouldShow: ({ editor: ed, state }) => {
2268
+ const { empty } = state.selection;
2269
+ if (empty || !ed.isEditable) return false;
2270
+ if (ed.isActive("image") || ed.isActive("pageBreak") || ed.isActive("codeBlock")) return false;
2271
+ return state.doc.textBetween(state.selection.from, state.selection.to).trim().length > 0;
2272
+ },
2273
+ children: [
2274
+ /* @__PURE__ */ jsx8(Btn, { label: "Bold", icon: "bold", toggle: true, active: s.bold, onClick: () => run().toggleBold().run() }),
2275
+ /* @__PURE__ */ jsx8(Btn, { label: "Italic", icon: "italic", toggle: true, active: s.italic, onClick: () => run().toggleItalic().run() }),
2276
+ /* @__PURE__ */ jsx8(Btn, { label: "Underline", icon: "underline", toggle: true, active: s.underline, onClick: () => run().toggleUnderline().run() }),
2277
+ /* @__PURE__ */ jsx8(Btn, { label: "Strikethrough", icon: "strike", toggle: true, active: s.strike, onClick: () => run().toggleStrike().run() }),
2278
+ /* @__PURE__ */ jsx8("span", { className: "june-doc__bubble-sep", "aria-hidden": "true" }),
2279
+ /* @__PURE__ */ jsx8(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ jsx8(Btn, { label: "Link", icon: "link", toggle: true, active: s.link || open, onClick: toggle }), children: (close) => /* @__PURE__ */ jsx8(LinkPanel, { editor, href: s.linkHref, isLink: s.link, close }) }),
2280
+ /* @__PURE__ */ jsx8(
2281
+ ColorPicker,
2282
+ {
2283
+ label: "Text color",
2284
+ icon: "palette",
2285
+ colors: TEXT_COLORS,
2286
+ current: s.color,
2287
+ resetLabel: "Default color",
2288
+ onPick: (c) => run().setColor(c).run(),
2289
+ onReset: () => run().unsetColor().run()
2290
+ }
2291
+ ),
2292
+ /* @__PURE__ */ jsx8(
2293
+ ColorPicker,
2294
+ {
2295
+ label: "Highlight",
2296
+ icon: "highlighter",
2297
+ colors: HIGHLIGHT_COLORS,
2298
+ current: s.highlight,
2299
+ resetLabel: "No highlight",
2300
+ onPick: (c) => run().setHighlight({ color: c }).run(),
2301
+ onReset: () => run().unsetHighlight().run()
2302
+ }
2303
+ ),
2304
+ /* @__PURE__ */ jsx8("span", { className: "june-doc__bubble-sep", "aria-hidden": "true" }),
2305
+ /* @__PURE__ */ jsx8(Btn, { label: "Clear formatting", icon: "eraser", onClick: () => run().unsetAllMarks().run() })
2306
+ ]
2307
+ }
2308
+ );
2309
+ }
2310
+
2311
+ // src/components/document-editor/document-content.tsx
2312
+ import { EditorContent } from "@tiptap/react";
2313
+ import { useEffect as useEffect4, useLayoutEffect, useRef as useRef5, useState as useState4 } from "react";
2314
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2315
+ var PAGE_WIDTH = 794;
2316
+ var PAGE_HEIGHT = 1123;
2317
+ var PAGE_GAP = 28;
2318
+ var CANVAS_PAD_X = 24;
2319
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect4 : useLayoutEffect;
2320
+ function geometry(width) {
2321
+ const ratio = width / PAGE_WIDTH;
2322
+ const height = Math.round(PAGE_HEIGHT * ratio);
2323
+ const padX = Math.round(Math.min(96, Math.max(20, width * 0.12)));
2324
+ const padY = Math.round(Math.min(96, Math.max(36, 96 * ratio)));
2325
+ return { width, height, padX, padY };
2326
+ }
2327
+ function DocumentContent({ editor, layout = "page", zoom = 1, showMarks = false, onMetrics }) {
2328
+ const canvas = useRef5(null);
2329
+ const sheet = useRef5(null);
2330
+ const [avail, setAvail] = useState4(PAGE_WIDTH);
2331
+ const [sheetH, setSheetH] = useState4(0);
2332
+ const [pages, setPages] = useState4(1);
2333
+ const metricsRef = useRef5(onMetrics);
2334
+ metricsRef.current = onMetrics;
2335
+ const last = useRef5({ page: 0, pages: 0 });
2336
+ const paged = layout === "page";
2337
+ const width = paged ? Math.max(260, Math.min(PAGE_WIDTH, Math.floor(avail / zoom))) : Math.max(260, Math.floor(avail / zoom));
2338
+ const g = geometry(width);
2339
+ const stride = g.height + PAGE_GAP;
2340
+ useIsoLayoutEffect(() => {
2341
+ const c = canvas.current;
2342
+ if (!c) return;
2343
+ const read = () => {
2344
+ const cs = typeof getComputedStyle === "function" ? getComputedStyle(c) : null;
2345
+ const padX = cs ? (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) : CANVAS_PAD_X * 2;
2346
+ const w = c.clientWidth - padX;
2347
+ if (w > 0) setAvail(w);
2348
+ };
2349
+ read();
2350
+ if (typeof ResizeObserver === "undefined") return;
2351
+ const ro = new ResizeObserver(read);
2352
+ ro.observe(c);
2353
+ return () => ro.disconnect();
2354
+ }, []);
2355
+ useEffect4(() => {
2356
+ if (!editor) return;
2357
+ setPaginationConfig(editor, {
2358
+ enabled: paged,
2359
+ pageHeight: g.height,
2360
+ gap: PAGE_GAP,
2361
+ padTop: g.padY,
2362
+ padBottom: g.padY
2363
+ });
2364
+ }, [editor, paged, g.height, g.padY, width]);
2365
+ useEffect4(() => {
2366
+ const c = canvas.current;
2367
+ const s = sheet.current;
2368
+ if (!editor || !c || !s) return;
2369
+ const prose = editor.view.dom;
2370
+ const report = () => {
2371
+ const total = paged ? getPageCount(editor) : 1;
2372
+ setPages(total);
2373
+ setSheetH(s.offsetHeight);
2374
+ const cRect = c.getBoundingClientRect();
2375
+ const sRect = s.getBoundingClientRect();
2376
+ const y = (cRect.top + c.clientHeight * 0.35 - sRect.top) / zoom;
2377
+ const page = paged ? Math.min(total, Math.max(1, Math.floor(y / stride) + 1)) : 1;
2378
+ if (last.current.page !== page || last.current.pages !== total) {
2379
+ last.current = { page, pages: total };
2380
+ metricsRef.current?.({ page, pages: total });
2381
+ }
2382
+ };
2383
+ report();
2384
+ editor.on("transaction", report);
2385
+ c.addEventListener("scroll", report, { passive: true });
2386
+ const onLoad = () => remeasurePagination(editor);
2387
+ prose.addEventListener("load", onLoad, true);
2388
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(report) : null;
2389
+ ro?.observe(s);
2390
+ return () => {
2391
+ editor.off("transaction", report);
2392
+ c.removeEventListener("scroll", report);
2393
+ prose.removeEventListener("load", onLoad, true);
2394
+ ro?.disconnect();
2395
+ };
2396
+ }, [editor, paged, zoom, stride]);
2397
+ const minHeight = paged ? pages * g.height + (pages - 1) * PAGE_GAP : void 0;
2398
+ const sheetStyle = {
2399
+ width,
2400
+ minHeight,
2401
+ transform: zoom === 1 ? void 0 : `scale(${zoom})`,
2402
+ "--jd-pad-x": `${g.padX}px`,
2403
+ "--jd-pad-y": `${g.padY}px`
2404
+ };
2405
+ const zoomerStyle = {
2406
+ width: width * zoom,
2407
+ height: zoom === 1 ? void 0 : Math.max(sheetH, minHeight ?? 0) * zoom
2408
+ };
2409
+ return /* @__PURE__ */ jsx9("div", { ref: canvas, className: "june-doc__content june-doc__canvas", "data-layout": layout, "data-marks": showMarks || void 0, children: /* @__PURE__ */ jsx9("div", { className: "june-doc__zoomer", style: zoomerStyle, children: /* @__PURE__ */ jsxs7("div", { ref: sheet, className: "june-doc__sheet", style: sheetStyle, children: [
2410
+ paged ? /* @__PURE__ */ jsx9("div", { className: "june-doc__pages", "aria-hidden": "true", children: Array.from({ length: pages }, (_, i) => /* @__PURE__ */ jsx9("div", { className: "june-doc__page", style: { top: i * stride, height: g.height }, children: /* @__PURE__ */ jsxs7("span", { className: "june-doc__page-num", children: [
2411
+ i + 1,
2412
+ " / ",
2413
+ pages
2414
+ ] }) }, i)) }) : null,
2415
+ /* @__PURE__ */ jsx9(EditorContent, { editor })
2416
+ ] }) }) });
2417
+ }
2418
+
2419
+ // src/components/document-editor/document-findbar.tsx
2420
+ import { useEditorState as useEditorState3 } from "@tiptap/react";
2421
+ import { useEffect as useEffect5, useRef as useRef6, useState as useState5 } from "react";
2422
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2423
+ function DocumentFindBar({ editor, editable, onClose, onNotice }) {
2424
+ const [term, setTerm] = useState5(() => {
2425
+ const { from, to, empty } = editor.state.selection;
2426
+ return empty ? "" : editor.state.doc.textBetween(from, to, " ").slice(0, 100);
2427
+ });
2428
+ const [replacement, setReplacement] = useState5("");
2429
+ const [caseSensitive, setCaseSensitive] = useState5(false);
2430
+ const input = useRef6(null);
2431
+ const info = useEditorState3({
2432
+ editor,
2433
+ selector: ({ editor: ed }) => {
2434
+ const s = getSearchState(ed);
2435
+ return { count: s?.matches.length ?? 0, current: s?.current ?? -1 };
2436
+ }
2437
+ });
2438
+ useEffect5(() => {
2439
+ input.current?.focus();
2440
+ input.current?.select();
2441
+ }, []);
2442
+ useEffect5(() => {
2443
+ setSearch(editor, { term, caseSensitive, current: 0 });
2444
+ }, [editor, term, caseSensitive]);
2445
+ useEffect5(() => () => {
2446
+ if (!editor.isDestroyed) setSearch(editor, { term: "" });
2447
+ }, [editor]);
2448
+ const count = info?.count ?? 0;
2449
+ const current = info?.current ?? -1;
2450
+ return /* @__PURE__ */ jsxs8(
2451
+ "div",
2452
+ {
2453
+ className: "june-doc__findbar",
2454
+ role: "search",
2455
+ onKeyDown: (e) => {
2456
+ if (e.key === "Escape") {
2457
+ e.stopPropagation();
2458
+ onClose();
2459
+ editor.commands.focus();
2460
+ }
2461
+ },
2462
+ children: [
2463
+ /* @__PURE__ */ jsxs8("div", { className: "june-doc__find-field", children: [
2464
+ /* @__PURE__ */ jsx10(Icon, { name: "search", size: 14 }),
2465
+ /* @__PURE__ */ jsx10(
2466
+ "input",
2467
+ {
2468
+ ref: input,
2469
+ className: "june-doc__find-input",
2470
+ "aria-label": "Find in document",
2471
+ placeholder: "Find in document",
2472
+ value: term,
2473
+ onChange: (e) => setTerm(e.target.value),
2474
+ onKeyDown: (e) => {
2475
+ if (e.key === "Enter") {
2476
+ e.preventDefault();
2477
+ gotoMatch(editor, e.shiftKey ? -1 : 1);
2478
+ }
2479
+ }
2480
+ }
2481
+ ),
2482
+ /* @__PURE__ */ jsx10("span", { className: "june-doc__find-count", "aria-live": "polite", children: term ? count ? `${current + 1} of ${count}` : "No results" : "" })
2483
+ ] }),
2484
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "june-doc__btn june-doc__btn--icon", "aria-label": "Previous match", title: "Previous (Shift+Enter)", disabled: !count, onClick: () => gotoMatch(editor, -1), children: /* @__PURE__ */ jsx10(Icon, { name: "chevronUp" }) }),
2485
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "june-doc__btn june-doc__btn--icon", "aria-label": "Next match", title: "Next (Enter)", disabled: !count, onClick: () => gotoMatch(editor, 1), children: /* @__PURE__ */ jsx10(Icon, { name: "chevronDown" }) }),
2486
+ /* @__PURE__ */ jsx10(
2487
+ "button",
2488
+ {
2489
+ type: "button",
2490
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--case",
2491
+ "aria-label": "Match case",
2492
+ title: "Match case",
2493
+ "aria-pressed": caseSensitive,
2494
+ "data-active": caseSensitive || void 0,
2495
+ onClick: () => setCaseSensitive((v) => !v),
2496
+ children: "Aa"
2497
+ }
2498
+ ),
2499
+ editable ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2500
+ /* @__PURE__ */ jsx10("span", { className: "june-doc__find-sep", "aria-hidden": "true" }),
2501
+ /* @__PURE__ */ jsx10(
2502
+ "input",
2503
+ {
2504
+ className: "june-doc__input june-doc__replace-input",
2505
+ "aria-label": "Replace with",
2506
+ placeholder: "Replace with",
2507
+ value: replacement,
2508
+ onChange: (e) => setReplacement(e.target.value),
2509
+ onKeyDown: (e) => {
2510
+ if (e.key === "Enter") {
2511
+ e.preventDefault();
2512
+ replaceCurrent(editor, replacement);
2513
+ }
2514
+ }
2515
+ }
2516
+ ),
2517
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "june-doc__btn", disabled: !count, onClick: () => replaceCurrent(editor, replacement), children: "Replace" }),
2518
+ /* @__PURE__ */ jsx10(
2519
+ "button",
2520
+ {
2521
+ type: "button",
2522
+ className: "june-doc__btn",
2523
+ disabled: !count,
2524
+ onClick: () => {
2525
+ const n = replaceAll(editor, replacement);
2526
+ onNotice(`Replaced ${n} ${n === 1 ? "match" : "matches"}`);
2527
+ },
2528
+ children: "Replace all"
2529
+ }
2530
+ )
2531
+ ] }) : null,
2532
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost june-doc__find-close", "aria-label": "Close find", onClick: onClose, children: /* @__PURE__ */ jsx10(Icon, { name: "close" }) })
2533
+ ]
2534
+ }
2535
+ );
2536
+ }
2537
+
2538
+ // src/components/document-editor/document-title.tsx
2539
+ import { jsx as jsx11 } from "react/jsx-runtime";
2540
+ function DocumentTitle({ value, disabled, onChange }) {
2541
+ return /* @__PURE__ */ jsx11(
2542
+ "input",
2543
+ {
2544
+ className: "june-doc__title",
2545
+ "aria-label": "Document title",
2546
+ value,
2547
+ disabled,
2548
+ placeholder: "Untitled document",
2549
+ onChange: (e) => onChange(e.target.value)
2550
+ }
2551
+ );
2552
+ }
2553
+
2554
+ // src/components/document-editor/document-header.tsx
2555
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2556
+ var FORMATS = {
2557
+ docx: { label: "Word document", hint: ".docx", icon: "file" },
2558
+ pdf: { label: "PDF", hint: "Print / Save as PDF", icon: "file" },
2559
+ html: { label: "Web page", hint: ".html", icon: "code" },
2560
+ markdown: { label: "Markdown", hint: ".md", icon: "file" },
2561
+ txt: { label: "Plain text", hint: ".txt", icon: "file" }
2562
+ };
2563
+ function DocumentHeader(props) {
2564
+ const { title, editable, isDraft, fullscreen, onToggleFullscreen, onTitle, exportFormats, canSave, showSave = canSave, saveState, onExport, onCopy, onSave } = props;
2565
+ return /* @__PURE__ */ jsxs9("div", { className: "june-doc__header", children: [
2566
+ fullscreen ? /* @__PURE__ */ jsx12("button", { type: "button", className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost", "aria-label": "Close full screen", title: "Close (Esc)", onClick: onToggleFullscreen, children: /* @__PURE__ */ jsx12(Icon, { name: "close", size: 18 }) }) : /* @__PURE__ */ jsx12("span", { className: "june-doc__doc-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx12(Icon, { name: "file", size: 16 }) }),
2567
+ /* @__PURE__ */ jsx12(DocumentTitle, { value: title, disabled: !editable, onChange: onTitle }),
2568
+ isDraft ? /* @__PURE__ */ jsxs9("span", { className: "june-doc__badge", children: [
2569
+ /* @__PURE__ */ jsx12(Icon, { name: "sparkles", size: 12 }),
2570
+ "AI draft"
2571
+ ] }) : null,
2572
+ /* @__PURE__ */ jsxs9("div", { className: "june-doc__header-actions", children: [
2573
+ showSave ? /* @__PURE__ */ jsxs9(
2574
+ "button",
2575
+ {
2576
+ type: "button",
2577
+ className: "june-doc__btn june-doc__btn--outline june-doc__save",
2578
+ "data-state": saveState,
2579
+ "aria-label": "Save",
2580
+ title: "Save (Ctrl+S)",
2581
+ disabled: saveState === "saving" || saveState === "saved" || saveState === "idle",
2582
+ onClick: onSave,
2583
+ children: [
2584
+ /* @__PURE__ */ jsx12(Icon, { name: saveState === "saved" ? "check" : "save" }),
2585
+ /* @__PURE__ */ jsx12("span", { children: saveState === "saving" ? "Saving\u2026" : saveState === "saved" ? "Saved" : "Save" })
2586
+ ]
2587
+ }
2588
+ ) : null,
2589
+ /* @__PURE__ */ jsx12(
2590
+ Dropdown,
2591
+ {
2592
+ align: "end",
2593
+ className: "june-doc__menu",
2594
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsxs9(
2595
+ "button",
2596
+ {
2597
+ type: "button",
2598
+ className: "june-doc__btn june-doc__btn--solid",
2599
+ "aria-label": "Download",
2600
+ "aria-haspopup": "menu",
2601
+ "aria-expanded": open,
2602
+ onClick: toggle,
2603
+ children: [
2604
+ /* @__PURE__ */ jsx12("span", { children: "Download" }),
2605
+ /* @__PURE__ */ jsx12(Icon, { name: "chevronDown", size: 14 })
2606
+ ]
2607
+ }
2608
+ ),
2609
+ children: (close) => /* @__PURE__ */ jsxs9("div", { role: "menu", "aria-label": "Download options", children: [
2610
+ exportFormats.map((f) => {
2611
+ const meta = FORMATS[f] ?? { label: f.toUpperCase(), hint: "", icon: "file" };
2612
+ return /* @__PURE__ */ jsxs9(
2613
+ "button",
2614
+ {
2615
+ type: "button",
2616
+ role: "menuitem",
2617
+ className: "june-doc__menu-item",
2618
+ onClick: () => {
2619
+ close();
2620
+ onExport(f);
2621
+ },
2622
+ children: [
2623
+ /* @__PURE__ */ jsx12(Icon, { name: meta.icon }),
2624
+ /* @__PURE__ */ jsx12("span", { className: "june-doc__menu-label", children: meta.label }),
2625
+ /* @__PURE__ */ jsx12("span", { className: "june-doc__menu-hint", children: meta.hint })
2626
+ ]
2627
+ },
2628
+ f
2629
+ );
2630
+ }),
2631
+ exportFormats.length ? /* @__PURE__ */ jsx12("div", { className: "june-doc__menu-sep", role: "separator" }) : null,
2632
+ /* @__PURE__ */ jsxs9(
2633
+ "button",
2634
+ {
2635
+ type: "button",
2636
+ role: "menuitem",
2637
+ className: "june-doc__menu-item",
2638
+ "aria-label": "Copy document",
2639
+ onClick: () => {
2640
+ close();
2641
+ onCopy();
2642
+ },
2643
+ children: [
2644
+ /* @__PURE__ */ jsx12(Icon, { name: "copy" }),
2645
+ /* @__PURE__ */ jsx12("span", { className: "june-doc__menu-label", children: "Copy to clipboard" })
2646
+ ]
2647
+ }
2648
+ )
2649
+ ] })
2650
+ }
2651
+ ),
2652
+ /* @__PURE__ */ jsx12(
2653
+ "button",
2654
+ {
2655
+ type: "button",
2656
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost",
2657
+ "aria-label": fullscreen ? "Exit full screen" : "Open full screen",
2658
+ title: fullscreen ? "Exit full screen" : "Open full screen",
2659
+ "aria-pressed": fullscreen,
2660
+ onClick: onToggleFullscreen,
2661
+ children: /* @__PURE__ */ jsx12(Icon, { name: "maximize" })
2662
+ }
2663
+ )
2664
+ ] })
2665
+ ] });
1100
2666
  }
1101
2667
 
1102
2668
  // src/components/document-editor/document-export.ts
1103
2669
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1104
- var PRINT_CSS = 'body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.6;color:#111827;max-width:780px;margin:32px auto;padding:0 20px}h1,h2,h3{line-height:1.25}table{border-collapse:collapse;width:100%}th,td{border:1px solid #d1d5db;padding:6px 10px;text-align:left}blockquote{border-left:4px solid #d1d5db;margin:1em 0;padding-left:1em;color:#4b5563}pre,code{font-family:ui-monospace,Menlo,monospace;background:#f3f4f6}pre{padding:12px;overflow:auto}@media print{body{margin:0;max-width:none}}';
2670
+ var PRINT_CSS = 'body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.6;color:#111827;max-width:780px;margin:32px auto;padding:0 20px}h1,h2,h3{line-height:1.25}table{border-collapse:collapse;width:100%}th,td{border:1px solid #d1d5db;padding:6px 10px;text-align:left}blockquote{border-left:4px solid #d1d5db;margin:1em 0;padding-left:1em;color:#4b5563}pre,code{font-family:ui-monospace,Menlo,monospace;background:#f3f4f6}pre{padding:12px;overflow:auto}img{max-width:100%;height:auto}mark{padding:0 .1em;border-radius:2px}div[data-page-break]{break-after:page;page-break-after:always;height:0}@media screen{div[data-page-break]{border-top:1px dashed #d1d5db;margin:24px 0}}@media print{body{margin:0;max-width:none}}';
1105
2671
  function buildHtmlDocument(title, bodyHtml) {
1106
2672
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(
1107
2673
  title
@@ -1114,7 +2680,7 @@ var textResult = (text, filename, mimeType) => ({
1114
2680
  blob: new Blob([text], { type: `${mimeType};charset=utf-8` })
1115
2681
  });
1116
2682
  var exportHtml = async (ctx) => {
1117
- const { docToHtml: docToHtml2 } = await import("./document-engine-C6ILFMRP.js");
2683
+ const { docToHtml: docToHtml2 } = await import("./document-engine-RKD5SLXR.js");
1118
2684
  return textResult(
1119
2685
  buildHtmlDocument(ctx.title, docToHtml2(ctx.json)),
1120
2686
  `${ctx.basename}.html`,
@@ -1122,7 +2688,7 @@ var exportHtml = async (ctx) => {
1122
2688
  );
1123
2689
  };
1124
2690
  var exportMarkdown = async (ctx) => {
1125
- const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-C6ILFMRP.js");
2691
+ const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-RKD5SLXR.js");
1126
2692
  return textResult(
1127
2693
  `# ${ctx.title}
1128
2694
 
@@ -1133,12 +2699,39 @@ ${docToMarkdown2(ctx.json)}
1133
2699
  );
1134
2700
  };
1135
2701
  var exportTxt = async (ctx) => {
1136
- const { docToText } = await import("./document-engine-C6ILFMRP.js");
2702
+ const { docToText } = await import("./document-engine-RKD5SLXR.js");
1137
2703
  return textResult(`${ctx.title}
1138
2704
 
1139
2705
  ${docToText(ctx.json)}
1140
2706
  `, `${ctx.basename}.txt`, "text/plain");
1141
2707
  };
2708
+ function toDocxColor(value) {
2709
+ if (typeof value !== "string") return void 0;
2710
+ const v = value.trim();
2711
+ let m = /^#([0-9a-f]{3})$/i.exec(v);
2712
+ if (m) return m[1].split("").map((c) => c + c).join("").toUpperCase();
2713
+ m = /^#([0-9a-f]{6})$/i.exec(v);
2714
+ if (m) return m[1].toUpperCase();
2715
+ m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(v);
2716
+ if (m) {
2717
+ return [m[1], m[2], m[3]].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("").toUpperCase();
2718
+ }
2719
+ return void 0;
2720
+ }
2721
+ function toHalfPoints(value) {
2722
+ if (typeof value !== "string") return void 0;
2723
+ const m = /^([\d.]+)\s*(px|pt)?$/i.exec(value.trim());
2724
+ if (!m) return void 0;
2725
+ const n = Number(m[1]);
2726
+ if (!Number.isFinite(n) || n <= 0) return void 0;
2727
+ const pt = m[2]?.toLowerCase() === "pt" ? n : n * 0.75;
2728
+ return Math.round(pt * 2);
2729
+ }
2730
+ function firstFontFamily(value) {
2731
+ if (typeof value !== "string") return void 0;
2732
+ const first = value.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
2733
+ return first || void 0;
2734
+ }
1142
2735
  var ORDERED_REF = "june-ordered";
1143
2736
  var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
1144
2737
  function runs(d, node, fmt) {
@@ -1152,14 +2745,21 @@ function runs(d, node, fmt) {
1152
2745
  const marks = child.marks ?? [];
1153
2746
  const has = (type) => marks.some((m) => m.type === type);
1154
2747
  const link = marks.find((m) => m.type === "link");
2748
+ const style = marks.find((m) => m.type === "textStyle")?.attrs ?? {};
2749
+ const highlight = marks.find((m) => m.type === "highlight");
2750
+ const fill = highlight ? toDocxColor(highlight.attrs?.color) ?? "FFF59D" : void 0;
1155
2751
  const run = new d.TextRun({
1156
2752
  text: child.text,
1157
2753
  bold: fmt.bold || has("bold") || void 0,
1158
2754
  italics: has("italic") || void 0,
1159
2755
  underline: has("underline") || link ? {} : void 0,
1160
2756
  strike: has("strike") || void 0,
1161
- font: has("code") ? "Courier New" : void 0,
1162
- color: link ? "0563C1" : void 0
2757
+ superScript: has("superscript") || void 0,
2758
+ subScript: has("subscript") || void 0,
2759
+ font: has("code") ? "Courier New" : firstFontFamily(style.fontFamily),
2760
+ size: toHalfPoints(style.fontSize),
2761
+ color: toDocxColor(style.color) ?? (link ? "0563C1" : void 0),
2762
+ shading: fill ? { type: d.ShadingType.CLEAR, fill, color: "auto" } : void 0
1163
2763
  });
1164
2764
  const href = typeof link?.attrs?.href === "string" ? sanitizeHref(link.attrs.href) : null;
1165
2765
  out.push(href ? new d.ExternalHyperlink({ link: href, children: [run] }) : run);
@@ -1228,555 +2828,257 @@ function table(d, node, state) {
1228
2828
  if (rows.length === 0) return new d.Paragraph("");
1229
2829
  return new d.Table({ rows, width: { size: 100, type: d.WidthType.PERCENTAGE } });
1230
2830
  }
1231
- function blocks(d, nodes, fmt, state) {
1232
- const headings = [d.HeadingLevel.HEADING_1, d.HeadingLevel.HEADING_2, d.HeadingLevel.HEADING_3];
1233
- const out = [];
1234
- for (const node of nodes ?? []) {
1235
- switch (node.type) {
1236
- case "paragraph":
1237
- out.push(paragraph(d, node, fmt));
1238
- break;
1239
- case "heading": {
1240
- const level = Math.min(Math.max(Number(node.attrs?.level) || 1, 1), 3);
1241
- out.push(paragraph(d, node, fmt, { heading: headings[level - 1] }));
1242
- break;
1243
- }
1244
- case "bulletList":
1245
- case "orderedList":
1246
- out.push(...list(d, node, fmt, 0, state));
1247
- break;
1248
- case "blockquote":
1249
- out.push(...blocks(d, node.content, { ...fmt, quote: true, indent: (fmt.indent ?? 0) + 360 }, state));
1250
- break;
1251
- case "codeBlock":
1252
- out.push(code(d, node));
1253
- break;
1254
- case "horizontalRule":
1255
- out.push(
1256
- new d.Paragraph({
1257
- border: { bottom: { color: "9CA3AF", space: 1, style: d.BorderStyle.SINGLE, size: 6 } },
1258
- spacing: { after: 120 }
2831
+ function image(d, node, state) {
2832
+ const src = typeof node.attrs?.src === "string" ? node.attrs.src : "";
2833
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
2834
+ const img = state.images.get(src);
2835
+ if (img) {
2836
+ return [
2837
+ new d.Paragraph({
2838
+ children: [
2839
+ new d.ImageRun({
2840
+ type: img.type,
2841
+ data: img.data,
2842
+ transformation: { width: img.width, height: img.height },
2843
+ altText: alt ? { name: alt, description: alt, title: alt } : void 0
1259
2844
  })
1260
- );
1261
- break;
1262
- case "table":
1263
- out.push(table(d, node, state));
1264
- break;
1265
- default:
1266
- if (node.content) out.push(...blocks(d, node.content, fmt, state));
1267
- }
2845
+ ],
2846
+ spacing: { after: 120 }
2847
+ })
2848
+ ];
1268
2849
  }
1269
- return out;
1270
- }
1271
- var exportDocx = async (ctx) => {
1272
- const d = await import("docx");
1273
- const state = { instance: 0 };
1274
- const children = [
1275
- new d.Paragraph({ text: ctx.title, heading: d.HeadingLevel.TITLE }),
1276
- ...blocks(d, ctx.json.content, {}, state)
1277
- ];
1278
- const doc = new d.Document({
1279
- title: ctx.title,
1280
- numbering: {
1281
- config: [
1282
- {
1283
- reference: ORDERED_REF,
1284
- levels: [0, 1, 2].map((level) => ({
1285
- level,
1286
- format: d.LevelFormat.DECIMAL,
1287
- text: `%${level + 1}.`,
1288
- alignment: d.AlignmentType.START,
1289
- style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
1290
- }))
1291
- }
1292
- ]
1293
- },
1294
- sections: [{ children }]
1295
- });
1296
- const blob = await d.Packer.toBlob(doc);
1297
- return { filename: `${ctx.basename}.docx`, mimeType: DOCX_MIME, blob };
1298
- };
1299
- function printHtml(html) {
1300
- return new Promise((resolve, reject) => {
1301
- const frame = document.createElement("iframe");
1302
- frame.setAttribute("aria-hidden", "true");
1303
- frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
1304
- const cleanup = () => setTimeout(() => frame.remove(), 500);
1305
- frame.onload = () => {
1306
- try {
1307
- const win = frame.contentWindow;
1308
- if (!win) throw new Error("The print frame is unavailable");
1309
- win.addEventListener("afterprint", cleanup);
1310
- win.focus();
1311
- win.print();
1312
- setTimeout(cleanup, 12e4);
1313
- resolve();
1314
- } catch (error) {
1315
- cleanup();
1316
- reject(error);
1317
- }
1318
- };
1319
- frame.srcdoc = html;
1320
- document.body.appendChild(frame);
1321
- });
1322
- }
1323
- var exportPdf = async (ctx) => {
1324
- const { docToHtml: docToHtml2 } = await import("./document-engine-C6ILFMRP.js");
1325
- await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
1326
- return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
1327
- };
1328
- var registry = /* @__PURE__ */ new Map([
1329
- ["html", exportHtml],
1330
- ["markdown", exportMarkdown],
1331
- ["txt", exportTxt],
1332
- ["docx", exportDocx],
1333
- ["pdf", exportPdf]
1334
- ]);
1335
- function registerDocumentExporter(format, exporter) {
1336
- registry.set(format, exporter);
1337
- }
1338
- function listDocumentExporters() {
1339
- return [...registry.keys()];
1340
- }
1341
- async function exportDocument(format, doc) {
1342
- const exporter = registry.get(format);
1343
- if (!exporter) throw new JuneError("EXPORT_ERROR", `No exporter is registered for "${format}"`);
1344
- try {
1345
- return await exporter({ ...doc, basename: slugify(doc.title) || "document" });
1346
- } catch (error) {
1347
- throw toJuneError(error, "EXPORT_ERROR", `Exporting ${format} failed`);
1348
- }
1349
- }
1350
- function downloadResult(result) {
1351
- if (!result.blob) return;
1352
- const url = URL.createObjectURL(result.blob);
1353
- const link = document.createElement("a");
1354
- link.href = url;
1355
- link.download = result.filename;
1356
- document.body.appendChild(link);
1357
- link.click();
1358
- link.remove();
1359
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
1360
- }
1361
- async function copyDocumentToClipboard(doc) {
1362
- const { docToHtml: docToHtml2, docToText } = await import("./document-engine-C6ILFMRP.js");
1363
- const html = docToHtml2(doc.json);
1364
- const text = `${doc.title}
1365
-
1366
- ${docToText(doc.json)}`;
1367
- try {
1368
- if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
1369
- await navigator.clipboard.write([
1370
- new ClipboardItem({
1371
- "text/html": new Blob([html], { type: "text/html" }),
1372
- "text/plain": new Blob([text], { type: "text/plain" })
2850
+ const href = src && !src.startsWith("data:") ? sanitizeHref(src) : null;
2851
+ if (!href) return [];
2852
+ return [
2853
+ new d.Paragraph({
2854
+ children: [
2855
+ new d.ExternalHyperlink({
2856
+ link: href,
2857
+ children: [new d.TextRun({ text: alt || href, color: "0563C1", underline: {} })]
1373
2858
  })
1374
- ]);
1375
- return true;
1376
- }
1377
- await navigator.clipboard.writeText(text);
1378
- return true;
1379
- } catch {
1380
- return false;
1381
- }
1382
- }
1383
-
1384
- // src/components/document-editor/document-merge.ts
1385
- var MAX_CELLS = 4e6;
1386
- function matchIndices(a, b) {
1387
- const n = a.length;
1388
- const m = b.length;
1389
- if (n * m > MAX_CELLS) return null;
1390
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
1391
- for (let i2 = n - 1; i2 >= 0; i2--) {
1392
- for (let j2 = m - 1; j2 >= 0; j2--) {
1393
- dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1394
- }
1395
- }
1396
- const matches = /* @__PURE__ */ new Map();
1397
- let i = 0;
1398
- let j = 0;
1399
- while (i < n && j < m) {
1400
- if (a[i] === b[j]) {
1401
- matches.set(i, j);
1402
- i++;
1403
- j++;
1404
- } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
1405
- else j++;
1406
- }
1407
- return matches;
2859
+ ]
2860
+ })
2861
+ ];
1408
2862
  }
1409
- var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
1410
- function mergeBlocks(base, local, incoming) {
1411
- if (same(local, base)) return { blocks: incoming, conflicts: 0 };
1412
- if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
1413
- const ml = matchIndices(base, local);
1414
- const mn = matchIndices(base, incoming);
1415
- if (!ml || !mn) return { blocks: null, conflicts: 1 };
1416
- const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
1417
- const out = [];
1418
- let conflicts = 0;
1419
- let pb = -1;
1420
- let pl = -1;
1421
- let pn = -1;
1422
- const region = (bEnd, lEnd, nEnd) => {
1423
- const B = base.slice(pb + 1, bEnd);
1424
- const L = local.slice(pl + 1, lEnd);
1425
- const N = incoming.slice(pn + 1, nEnd);
1426
- if (B.length === L.length && L.length === N.length) {
1427
- for (let k = 0; k < B.length; k++) {
1428
- if (L[k] === B[k]) out.push(N[k]);
1429
- else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
1430
- else {
1431
- conflicts++;
1432
- out.push(L[k]);
1433
- }
1434
- }
1435
- return;
1436
- }
1437
- if (same(L, B)) out.push(...N);
1438
- else if (same(N, B) || same(L, N)) out.push(...L);
1439
- else {
1440
- conflicts++;
1441
- out.push(...L);
1442
- }
1443
- };
1444
- for (const a of anchors) {
1445
- const la = ml.get(a);
1446
- const na = mn.get(a);
1447
- region(a, la, na);
1448
- out.push(local[la]);
1449
- pb = a;
1450
- pl = la;
1451
- pn = na;
2863
+ function collectImages(nodes, out) {
2864
+ for (const node of nodes ?? []) {
2865
+ const src = node.attrs?.src;
2866
+ if (node.type === "image" && typeof src === "string" && src && !out.has(src)) out.set(src, node.attrs);
2867
+ if (node.content) collectImages(node.content, out);
1452
2868
  }
1453
- region(base.length, local.length, incoming.length);
1454
- return { blocks: out, conflicts };
1455
- }
1456
- function mergeDocuments(base, local, incoming) {
1457
- const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
1458
- const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
1459
- if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
1460
- if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
1461
- return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
1462
- }
1463
-
1464
- // src/components/document-editor/document-store.ts
1465
- var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
1466
- function createDocumentStore() {
1467
- const records = /* @__PURE__ */ new Map();
1468
- const listeners = /* @__PURE__ */ new Set();
1469
- const emit = () => listeners.forEach((listener) => listener());
1470
- const put = (record) => {
1471
- records.set(record.id, record);
1472
- emit();
1473
- };
1474
- const fresh2 = (doc) => ({
1475
- id: doc.id,
1476
- title: doc.title,
1477
- documentType: doc.documentType,
1478
- status: doc.status,
1479
- json: doc.json,
1480
- baseJson: doc.json,
1481
- baseTitle: doc.title,
1482
- lastIncomingKey: incomingKey(doc),
1483
- revision: 0,
1484
- origin: "external",
1485
- pending: null,
1486
- updatedAt: Date.now()
1487
- });
1488
- return {
1489
- get: (id) => records.get(id),
1490
- list: () => [...records.values()],
1491
- subscribe: (listener) => {
1492
- listeners.add(listener);
1493
- return () => {
1494
- listeners.delete(listener);
1495
- };
1496
- },
1497
- receive(incoming) {
1498
- const current = records.get(incoming.id);
1499
- if (!current) {
1500
- put(fresh2(incoming));
1501
- return "created";
1502
- }
1503
- const key = incomingKey(incoming);
1504
- if (current.lastIncomingKey === key) return "unchanged";
1505
- const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
1506
- const userEditedTitle = current.title !== current.baseTitle;
1507
- const title = userEditedTitle ? current.title : incoming.title;
1508
- const meta = {
1509
- documentType: incoming.documentType ?? current.documentType,
1510
- status: incoming.status ?? current.status
1511
- };
1512
- if (!userEditedBody) {
1513
- put({
1514
- ...current,
1515
- ...meta,
1516
- title,
1517
- baseTitle: incoming.title,
1518
- json: incoming.json,
1519
- baseJson: incoming.json,
1520
- lastIncomingKey: key,
1521
- revision: current.revision + 1,
1522
- origin: "external",
1523
- pending: null,
1524
- updatedAt: Date.now()
1525
- });
1526
- return "applied";
1527
- }
1528
- const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
1529
- if (conflicts === 0) {
1530
- put({
1531
- ...current,
1532
- ...meta,
1533
- title,
1534
- baseTitle: incoming.title,
1535
- json: doc,
1536
- baseJson: incoming.json,
1537
- lastIncomingKey: key,
1538
- revision: current.revision + 1,
1539
- origin: "external",
1540
- pending: null,
1541
- updatedAt: Date.now()
1542
- });
1543
- return "merged";
1544
- }
1545
- put({
1546
- ...current,
1547
- ...meta,
1548
- lastIncomingKey: key,
1549
- pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
1550
- });
1551
- return "conflict";
1552
- },
1553
- hydrate(doc) {
1554
- put(fresh2(doc));
1555
- },
1556
- edit(id, json, origin) {
1557
- const current = records.get(id);
1558
- if (!current || docKey(current.json) === docKey(json)) return;
1559
- put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
1560
- },
1561
- setTitle(id, title) {
1562
- const current = records.get(id);
1563
- if (!current || current.title === title) return;
1564
- put({ ...current, title, updatedAt: Date.now() });
1565
- },
1566
- resolve(id, choice) {
1567
- const current = records.get(id);
1568
- const pending = current?.pending;
1569
- if (!current || !pending) return;
1570
- if (choice === "keep") {
1571
- put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
1572
- return;
1573
- }
1574
- const json = choice === "merge" ? pending.merged : pending.json;
1575
- const title = choice === "merge" ? current.title : pending.title;
1576
- put({
1577
- ...current,
1578
- title,
1579
- json,
1580
- baseJson: pending.json,
1581
- baseTitle: pending.title,
1582
- revision: current.revision + 1,
1583
- origin: "external",
1584
- pending: null,
1585
- updatedAt: Date.now()
1586
- });
1587
- },
1588
- remove(id) {
1589
- if (records.delete(id)) emit();
1590
- }
1591
- };
2869
+ return out;
1592
2870
  }
1593
- var defaultStore = null;
1594
- function getDefaultDocumentStore() {
1595
- if (!defaultStore) defaultStore = createDocumentStore();
1596
- return defaultStore;
2871
+ var IMAGE_TYPES = {
2872
+ "image/png": "png",
2873
+ "image/jpeg": "jpg",
2874
+ "image/jpg": "jpg",
2875
+ "image/gif": "gif",
2876
+ "image/bmp": "bmp"
2877
+ };
2878
+ var MAX_IMAGE_WIDTH = 600;
2879
+ function naturalSize(blob) {
2880
+ const fallback = { width: 400, height: 300 };
2881
+ return new Promise((resolve) => {
2882
+ if (typeof Image === "undefined" || typeof URL.createObjectURL !== "function") return resolve(fallback);
2883
+ const url = URL.createObjectURL(blob);
2884
+ const img = new Image();
2885
+ const done = (size) => {
2886
+ URL.revokeObjectURL(url);
2887
+ resolve(size);
2888
+ };
2889
+ img.onload = () => done({ width: img.naturalWidth || 400, height: img.naturalHeight || 300 });
2890
+ img.onerror = () => done(fallback);
2891
+ img.src = url;
2892
+ });
1597
2893
  }
1598
-
1599
- // src/components/document-editor/document-title.tsx
1600
- import { jsx as jsx6 } from "react/jsx-runtime";
1601
- function DocumentTitle({ value, disabled, onChange }) {
1602
- return /* @__PURE__ */ jsx6(
1603
- "input",
1604
- {
1605
- className: "june-doc__title",
1606
- "aria-label": "Document title",
1607
- value,
1608
- disabled,
1609
- placeholder: "Untitled document",
1610
- onChange: (e) => onChange(e.target.value)
2894
+ async function loadImage(src, attrs) {
2895
+ try {
2896
+ const res = await fetch(src);
2897
+ if (!res.ok) return null;
2898
+ const blob = await res.blob();
2899
+ const type = IMAGE_TYPES[blob.type.toLowerCase()];
2900
+ if (!type) return null;
2901
+ const natural = await naturalSize(blob);
2902
+ let width = Number(attrs?.width) || natural.width;
2903
+ let height = Number(attrs?.height) || natural.height * width / natural.width;
2904
+ if (width > MAX_IMAGE_WIDTH) {
2905
+ height = height * MAX_IMAGE_WIDTH / width;
2906
+ width = MAX_IMAGE_WIDTH;
1611
2907
  }
2908
+ return { data: new Uint8Array(await blob.arrayBuffer()), type, width: Math.round(width), height: Math.round(height) };
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ }
2913
+ async function resolveImages(doc) {
2914
+ const map = /* @__PURE__ */ new Map();
2915
+ if (typeof fetch !== "function") return map;
2916
+ const found = collectImages(doc.content, /* @__PURE__ */ new Map());
2917
+ await Promise.all(
2918
+ [...found].map(async ([src, attrs]) => {
2919
+ const info = await loadImage(src, attrs);
2920
+ if (info) map.set(src, info);
2921
+ })
1612
2922
  );
2923
+ return map;
1613
2924
  }
1614
-
1615
- // src/components/document-editor/document-toolbar.tsx
1616
- import { useEditorState } from "@tiptap/react";
1617
- import { useState as useState2 } from "react";
1618
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
1619
- var FORMAT_LABELS = {
1620
- html: "HTML",
1621
- markdown: "Markdown",
1622
- txt: "Plain text",
1623
- docx: "Word (.docx)",
1624
- pdf: "PDF (print)"
1625
- };
1626
- function Btn({ label, active, disabled, toggle, onClick, children }) {
1627
- return /* @__PURE__ */ jsx7(
1628
- "button",
1629
- {
1630
- type: "button",
1631
- className: "june-doc__btn",
1632
- "aria-label": label,
1633
- title: label,
1634
- "aria-pressed": toggle ? !!active : void 0,
1635
- "data-active": active || void 0,
1636
- disabled,
1637
- onMouseDown: (e) => e.preventDefault(),
1638
- onClick,
1639
- children
2925
+ function blocks(d, nodes, fmt, state) {
2926
+ const headings = [d.HeadingLevel.HEADING_1, d.HeadingLevel.HEADING_2, d.HeadingLevel.HEADING_3];
2927
+ const out = [];
2928
+ for (const node of nodes ?? []) {
2929
+ switch (node.type) {
2930
+ case "paragraph":
2931
+ out.push(paragraph(d, node, fmt));
2932
+ break;
2933
+ case "heading": {
2934
+ const level = Math.min(Math.max(Number(node.attrs?.level) || 1, 1), 3);
2935
+ out.push(paragraph(d, node, fmt, { heading: headings[level - 1] }));
2936
+ break;
2937
+ }
2938
+ case "bulletList":
2939
+ case "orderedList":
2940
+ out.push(...list(d, node, fmt, 0, state));
2941
+ break;
2942
+ case "blockquote":
2943
+ out.push(...blocks(d, node.content, { ...fmt, quote: true, indent: (fmt.indent ?? 0) + 360 }, state));
2944
+ break;
2945
+ case "codeBlock":
2946
+ out.push(code(d, node));
2947
+ break;
2948
+ case "horizontalRule":
2949
+ out.push(
2950
+ new d.Paragraph({
2951
+ border: { bottom: { color: "9CA3AF", space: 1, style: d.BorderStyle.SINGLE, size: 6 } },
2952
+ spacing: { after: 120 }
2953
+ })
2954
+ );
2955
+ break;
2956
+ case "table":
2957
+ out.push(table(d, node, state));
2958
+ break;
2959
+ case "pageBreak":
2960
+ out.push(new d.Paragraph({ children: [new d.PageBreak()] }));
2961
+ break;
2962
+ case "image":
2963
+ out.push(...image(d, node, state));
2964
+ break;
2965
+ default:
2966
+ if (node.content) out.push(...blocks(d, node.content, fmt, state));
1640
2967
  }
1641
- );
2968
+ }
2969
+ return out;
1642
2970
  }
1643
- function DocumentToolbar(props) {
1644
- const { editor, editable, exportFormats, canSave, saveState, onExport, onCopy, onSave } = props;
1645
- const [linkOpen, setLinkOpen] = useState2(false);
1646
- const [linkValue, setLinkValue] = useState2("");
1647
- const s = useEditorState({
1648
- editor,
1649
- selector: ({ editor: ed }) => ed ? {
1650
- bold: ed.isActive("bold"),
1651
- italic: ed.isActive("italic"),
1652
- underline: ed.isActive("underline"),
1653
- strike: ed.isActive("strike"),
1654
- bullet: ed.isActive("bulletList"),
1655
- ordered: ed.isActive("orderedList"),
1656
- quote: ed.isActive("blockquote"),
1657
- link: ed.isActive("link"),
1658
- linkHref: ed.getAttributes("link").href ?? "",
1659
- h1: ed.isActive("heading", { level: 1 }),
1660
- h2: ed.isActive("heading", { level: 2 }),
1661
- h3: ed.isActive("heading", { level: 3 }),
1662
- left: ed.isActive({ textAlign: "left" }),
1663
- center: ed.isActive({ textAlign: "center" }),
1664
- right: ed.isActive({ textAlign: "right" }),
1665
- justify: ed.isActive({ textAlign: "justify" }),
1666
- inTable: ed.isActive("table"),
1667
- canUndo: ed.can().undo(),
1668
- canRedo: ed.can().redo()
1669
- } : null
1670
- });
1671
- const run = () => editor?.chain().focus();
1672
- const off = !editor || !editable;
1673
- const heading = s?.h1 ? "1" : s?.h2 ? "2" : s?.h3 ? "3" : "p";
1674
- const openLink = () => {
1675
- setLinkValue(s?.linkHref ?? "");
1676
- setLinkOpen(true);
1677
- };
1678
- const applyLink = () => {
1679
- const href = sanitizeHref(linkValue);
1680
- if (href) run()?.extendMarkRange("link").setLink({ href }).run();
1681
- setLinkOpen(false);
1682
- };
1683
- const removeLink = () => {
1684
- run()?.extendMarkRange("link").unsetLink().run();
1685
- setLinkOpen(false);
1686
- };
1687
- return /* @__PURE__ */ jsxs3("div", { className: "june-doc__toolbar", role: "toolbar", "aria-label": "Document formatting", children: [
1688
- editable ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1689
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
1690
- /* @__PURE__ */ jsx7(Btn, { label: "Undo", disabled: off || !s?.canUndo, onClick: () => run()?.undo().run(), children: "\u21B6" }),
1691
- /* @__PURE__ */ jsx7(Btn, { label: "Redo", disabled: off || !s?.canRedo, onClick: () => run()?.redo().run(), children: "\u21B7" })
1692
- ] }),
1693
- /* @__PURE__ */ jsx7("div", { className: "june-doc__group", children: /* @__PURE__ */ jsxs3(
1694
- "select",
1695
- {
1696
- className: "june-doc__select",
1697
- "aria-label": "Text style",
1698
- disabled: off,
1699
- value: heading,
1700
- onChange: (e) => {
1701
- const v = e.target.value;
1702
- if (v === "p") run()?.setParagraph().run();
1703
- else run()?.toggleHeading({ level: Number(v) }).run();
1704
- },
1705
- children: [
1706
- /* @__PURE__ */ jsx7("option", { value: "p", children: "Paragraph" }),
1707
- /* @__PURE__ */ jsx7("option", { value: "1", children: "Heading 1" }),
1708
- /* @__PURE__ */ jsx7("option", { value: "2", children: "Heading 2" }),
1709
- /* @__PURE__ */ jsx7("option", { value: "3", children: "Heading 3" })
1710
- ]
1711
- }
1712
- ) }),
1713
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
1714
- /* @__PURE__ */ jsx7(Btn, { label: "Bold", toggle: true, active: s?.bold, disabled: off, onClick: () => run()?.toggleBold().run(), children: /* @__PURE__ */ jsx7("b", { children: "B" }) }),
1715
- /* @__PURE__ */ jsx7(Btn, { label: "Italic", toggle: true, active: s?.italic, disabled: off, onClick: () => run()?.toggleItalic().run(), children: /* @__PURE__ */ jsx7("i", { children: "I" }) }),
1716
- /* @__PURE__ */ jsx7(Btn, { label: "Underline", toggle: true, active: s?.underline, disabled: off, onClick: () => run()?.toggleUnderline().run(), children: /* @__PURE__ */ jsx7("u", { children: "U" }) }),
1717
- /* @__PURE__ */ jsx7(Btn, { label: "Strikethrough", toggle: true, active: s?.strike, disabled: off, onClick: () => run()?.toggleStrike().run(), children: /* @__PURE__ */ jsx7("s", { children: "S" }) })
1718
- ] }),
1719
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
1720
- /* @__PURE__ */ jsx7(Btn, { label: "Bullet list", toggle: true, active: s?.bullet, disabled: off, onClick: () => run()?.toggleBulletList().run(), children: "\u2022 List" }),
1721
- /* @__PURE__ */ jsx7(Btn, { label: "Numbered list", toggle: true, active: s?.ordered, disabled: off, onClick: () => run()?.toggleOrderedList().run(), children: "1. List" }),
1722
- /* @__PURE__ */ jsx7(Btn, { label: "Blockquote", toggle: true, active: s?.quote, disabled: off, onClick: () => run()?.toggleBlockquote().run(), children: "Quote" }),
1723
- /* @__PURE__ */ jsx7(Btn, { label: "Link", toggle: true, active: s?.link, disabled: off, onClick: openLink, children: "Link" })
1724
- ] }),
1725
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
1726
- /* @__PURE__ */ jsx7(Btn, { label: "Align left", toggle: true, active: s?.left, disabled: off, onClick: () => run()?.setTextAlign("left").run(), children: "L" }),
1727
- /* @__PURE__ */ jsx7(Btn, { label: "Align center", toggle: true, active: s?.center, disabled: off, onClick: () => run()?.setTextAlign("center").run(), children: "C" }),
1728
- /* @__PURE__ */ jsx7(Btn, { label: "Align right", toggle: true, active: s?.right, disabled: off, onClick: () => run()?.setTextAlign("right").run(), children: "R" }),
1729
- /* @__PURE__ */ jsx7(Btn, { label: "Justify", toggle: true, active: s?.justify, disabled: off, onClick: () => run()?.setTextAlign("justify").run(), children: "J" })
1730
- ] }),
1731
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
1732
- /* @__PURE__ */ jsx7(Btn, { label: "Insert table", disabled: off, onClick: () => run()?.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: "Table" }),
1733
- s?.inTable ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1734
- /* @__PURE__ */ jsx7(Btn, { label: "Add row below", disabled: off, onClick: () => run()?.addRowAfter().run(), children: "+Row" }),
1735
- /* @__PURE__ */ jsx7(Btn, { label: "Add column right", disabled: off, onClick: () => run()?.addColumnAfter().run(), children: "+Col" }),
1736
- /* @__PURE__ */ jsx7(Btn, { label: "Delete row", disabled: off, onClick: () => run()?.deleteRow().run(), children: "\u2212Row" }),
1737
- /* @__PURE__ */ jsx7(Btn, { label: "Delete column", disabled: off, onClick: () => run()?.deleteColumn().run(), children: "\u2212Col" }),
1738
- /* @__PURE__ */ jsx7(Btn, { label: "Delete table", disabled: off, onClick: () => run()?.deleteTable().run(), children: "Del table" })
1739
- ] }) : null
1740
- ] })
1741
- ] }) : null,
1742
- /* @__PURE__ */ jsxs3("div", { className: "june-doc__group june-doc__group--end", children: [
1743
- /* @__PURE__ */ jsx7(Btn, { label: "Copy document", onClick: onCopy, children: "Copy" }),
1744
- exportFormats.length > 0 ? /* @__PURE__ */ jsxs3(
1745
- "select",
1746
- {
1747
- className: "june-doc__select",
1748
- "aria-label": "Export document",
1749
- value: "",
1750
- onChange: (e) => e.target.value && onExport(e.target.value),
1751
- children: [
1752
- /* @__PURE__ */ jsx7("option", { value: "", children: "Export\u2026" }),
1753
- exportFormats.map((f) => /* @__PURE__ */ jsx7("option", { value: f, children: FORMAT_LABELS[f] ?? f }, f))
1754
- ]
1755
- }
1756
- ) : null,
1757
- canSave ? /* @__PURE__ */ jsx7(Btn, { label: "Save", disabled: saveState === "saving" || saveState === "saved" || saveState === "idle", onClick: onSave, children: "Save" }) : null
1758
- ] }),
1759
- linkOpen ? /* @__PURE__ */ jsxs3("div", { className: "june-doc__linkbar", children: [
1760
- /* @__PURE__ */ jsx7(
1761
- "input",
2971
+ var exportDocx = async (ctx) => {
2972
+ const d = await import("docx");
2973
+ const state = { instance: 0, images: await resolveImages(ctx.json) };
2974
+ const children = [
2975
+ new d.Paragraph({ text: ctx.title, heading: d.HeadingLevel.TITLE }),
2976
+ ...blocks(d, ctx.json.content, {}, state)
2977
+ ];
2978
+ const doc = new d.Document({
2979
+ title: ctx.title,
2980
+ numbering: {
2981
+ config: [
1762
2982
  {
1763
- "aria-label": "Link address",
1764
- className: "june-doc__input",
1765
- placeholder: "https://example.com",
1766
- value: linkValue,
1767
- autoFocus: true,
1768
- onChange: (e) => setLinkValue(e.target.value),
1769
- onKeyDown: (e) => {
1770
- if (e.key === "Enter") applyLink();
1771
- if (e.key === "Escape") setLinkOpen(false);
1772
- }
2983
+ reference: ORDERED_REF,
2984
+ levels: [0, 1, 2].map((level) => ({
2985
+ level,
2986
+ format: d.LevelFormat.DECIMAL,
2987
+ text: `%${level + 1}.`,
2988
+ alignment: d.AlignmentType.START,
2989
+ style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
2990
+ }))
1773
2991
  }
1774
- ),
1775
- /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: applyLink, children: "Apply" }),
1776
- s?.link ? /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: removeLink, children: "Remove" }) : null,
1777
- /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: () => setLinkOpen(false), children: "Cancel" })
1778
- ] }) : null
1779
- ] });
2992
+ ]
2993
+ },
2994
+ sections: [{ children }]
2995
+ });
2996
+ const blob = await d.Packer.toBlob(doc);
2997
+ return { filename: `${ctx.basename}.docx`, mimeType: DOCX_MIME, blob };
2998
+ };
2999
+ function printHtml(html) {
3000
+ return new Promise((resolve, reject) => {
3001
+ const frame = document.createElement("iframe");
3002
+ frame.setAttribute("aria-hidden", "true");
3003
+ frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
3004
+ const cleanup = () => setTimeout(() => frame.remove(), 500);
3005
+ frame.onload = () => {
3006
+ try {
3007
+ const win = frame.contentWindow;
3008
+ if (!win) throw new Error("The print frame is unavailable");
3009
+ win.addEventListener("afterprint", cleanup);
3010
+ win.focus();
3011
+ win.print();
3012
+ setTimeout(cleanup, 12e4);
3013
+ resolve();
3014
+ } catch (error) {
3015
+ cleanup();
3016
+ reject(error);
3017
+ }
3018
+ };
3019
+ frame.srcdoc = html;
3020
+ document.body.appendChild(frame);
3021
+ });
3022
+ }
3023
+ var exportPdf = async (ctx) => {
3024
+ const { docToHtml: docToHtml2 } = await import("./document-engine-RKD5SLXR.js");
3025
+ await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
3026
+ return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
3027
+ };
3028
+ var registry = /* @__PURE__ */ new Map([
3029
+ ["html", exportHtml],
3030
+ ["markdown", exportMarkdown],
3031
+ ["txt", exportTxt],
3032
+ ["docx", exportDocx],
3033
+ ["pdf", exportPdf]
3034
+ ]);
3035
+ function registerDocumentExporter(format, exporter) {
3036
+ registry.set(format, exporter);
3037
+ }
3038
+ function listDocumentExporters() {
3039
+ return [...registry.keys()];
3040
+ }
3041
+ async function exportDocument(format, doc) {
3042
+ const exporter = registry.get(format);
3043
+ if (!exporter) throw new JuneError("EXPORT_ERROR", `No exporter is registered for "${format}"`);
3044
+ try {
3045
+ return await exporter({ ...doc, basename: slugify(doc.title) || "document" });
3046
+ } catch (error) {
3047
+ throw toJuneError(error, "EXPORT_ERROR", `Exporting ${format} failed`);
3048
+ }
3049
+ }
3050
+ function downloadResult(result) {
3051
+ if (!result.blob) return;
3052
+ const url = URL.createObjectURL(result.blob);
3053
+ const link = document.createElement("a");
3054
+ link.href = url;
3055
+ link.download = result.filename;
3056
+ document.body.appendChild(link);
3057
+ link.click();
3058
+ link.remove();
3059
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
3060
+ }
3061
+ async function copyDocumentToClipboard(doc) {
3062
+ const { docToHtml: docToHtml2, docToText } = await import("./document-engine-RKD5SLXR.js");
3063
+ const html = docToHtml2(doc.json);
3064
+ const text = `${doc.title}
3065
+
3066
+ ${docToText(doc.json)}`;
3067
+ try {
3068
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
3069
+ await navigator.clipboard.write([
3070
+ new ClipboardItem({
3071
+ "text/html": new Blob([html], { type: "text/html" }),
3072
+ "text/plain": new Blob([text], { type: "text/plain" })
3073
+ })
3074
+ ]);
3075
+ return true;
3076
+ }
3077
+ await navigator.clipboard.writeText(text);
3078
+ return true;
3079
+ } catch {
3080
+ return false;
3081
+ }
1780
3082
  }
1781
3083
 
1782
3084
  // src/components/document-editor/document-types.ts
@@ -1789,7 +3091,7 @@ var ALL_EXPORT_FORMATS = [
1789
3091
  ];
1790
3092
 
1791
3093
  // src/components/document-editor/document-editor.tsx
1792
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
3094
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1793
3095
  var snapshotOf = (r) => ({
1794
3096
  documentId: r.id,
1795
3097
  title: r.title,
@@ -1804,6 +3106,8 @@ var SAVE_LABEL = {
1804
3106
  saved: "Saved",
1805
3107
  error: "Save failed"
1806
3108
  };
3109
+ var ZOOM_STEPS = [0.5, 0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
3110
+ var stepZoom = (z2, dir) => dir > 0 ? ZOOM_STEPS.find((s) => s > z2 + 1e-3) ?? z2 : [...ZOOM_STEPS].reverse().find((s) => s < z2 - 1e-3) ?? z2;
1807
3111
  function DocumentEditorImpl(props) {
1808
3112
  const {
1809
3113
  documentId,
@@ -1818,7 +3122,7 @@ function DocumentEditorImpl(props) {
1818
3122
  } = props;
1819
3123
  const ctxOptions = useDocumentOptions();
1820
3124
  const options = useMemo4(() => ({ ...ctxOptions, ...props.options }), [ctxOptions, props.options]);
1821
- const optionsRef = useRef3(options);
3125
+ const optionsRef = useRef7(options);
1822
3126
  optionsRef.current = options;
1823
3127
  const store = useMemo4(() => options.store ?? getDefaultDocumentStore(), [options.store]);
1824
3128
  const instanceId = useId();
@@ -1833,22 +3137,37 @@ function DocumentEditorImpl(props) {
1833
3137
  return { json: null, error: toJuneError(e, "DOCUMENT_ERROR") };
1834
3138
  }
1835
3139
  }, [contentKey]);
1836
- useEffect2(() => {
3140
+ useEffect6(() => {
1837
3141
  if (parsed.error) optionsRef.current.onError?.(parsed.error);
1838
3142
  }, [parsed.error]);
1839
3143
  const title = useDocumentValue(store, documentId, (r) => r?.title ?? titleProp);
1840
3144
  const pending = useDocumentValue(store, documentId, (r) => r?.pending ?? null);
1841
3145
  const words = useDocumentValue(store, documentId, (r) => r ? wordCountOf(r.json) : 0);
1842
3146
  const recordStatus = useDocumentValue(store, documentId, (r) => r?.status ?? status);
1843
- const [saveState, setSaveState] = useState3("idle");
1844
- const [notice, setNotice] = useState3(null);
1845
- const lastSavedKey = useRef3("");
1846
- const seenRevision = useRef3(store.get(documentId)?.revision ?? 0);
1847
- const readyRef = useRef3(false);
3147
+ const [saveState, setSaveState] = useState6("idle");
3148
+ const [notice, setNotice] = useState6(null);
3149
+ const [fullscreen, setFullscreen] = useState6(false);
3150
+ const [expanded, setExpanded] = useState6(true);
3151
+ const [showMarks, setShowMarks] = useState6(false);
3152
+ const [layout, setLayout] = useState6("page");
3153
+ const [findOpen, setFindOpen] = useState6(false);
3154
+ const [zoom, setZoom] = useState6(1);
3155
+ const [metrics, setMetrics] = useState6({ page: 1, pages: 1 });
3156
+ useEffect6(() => {
3157
+ if (!fullscreen || typeof document === "undefined") return;
3158
+ const prev = document.body.style.overflow;
3159
+ document.body.style.overflow = "hidden";
3160
+ return () => {
3161
+ document.body.style.overflow = prev;
3162
+ };
3163
+ }, [fullscreen]);
3164
+ const lastSavedKey = useRef7("");
3165
+ const seenRevision = useRef7(store.get(documentId)?.revision ?? 0);
3166
+ const readyRef = useRef7(false);
1848
3167
  const flash = useCallback3((message) => {
1849
3168
  setNotice(message);
1850
3169
  }, []);
1851
- useEffect2(() => {
3170
+ useEffect6(() => {
1852
3171
  if (!notice) return;
1853
3172
  const t = setTimeout(() => setNotice(null), 3e3);
1854
3173
  return () => clearTimeout(t);
@@ -1860,18 +3179,23 @@ function DocumentEditorImpl(props) {
1860
3179
  },
1861
3180
  [store, documentId]
1862
3181
  );
1863
- const saveRef = useRef3(async () => {
3182
+ const saveRef = useRef7(async () => {
1864
3183
  });
1865
3184
  const autosave = useMemo4(() => {
1866
3185
  const delay = options.autosaveDelay ?? 1500;
1867
3186
  return delay > 0 ? debounce(() => void saveRef.current(), delay) : null;
1868
3187
  }, [options.autosaveDelay]);
1869
- useEffect2(() => () => autosave?.flush(), [autosave]);
3188
+ useEffect6(() => () => autosave?.flush(), [autosave]);
1870
3189
  saveRef.current = async () => {
1871
3190
  const record = store.get(documentId);
1872
3191
  const onSave = optionsRef.current.onSave;
1873
- if (!record || !onSave) return;
3192
+ if (!record) return;
1874
3193
  const key = JSON.stringify([record.title, docKey(record.json)]);
3194
+ if (!onSave) {
3195
+ lastSavedKey.current = key;
3196
+ setSaveState("saved");
3197
+ return;
3198
+ }
1875
3199
  if (key === lastSavedKey.current) {
1876
3200
  setSaveState("saved");
1877
3201
  return;
@@ -1892,14 +3216,25 @@ function DocumentEditorImpl(props) {
1892
3216
  optionsRef.current.onError?.(toJuneError(error, "DOCUMENT_ERROR", "Saving the document failed"));
1893
3217
  }
1894
3218
  };
1895
- const markDirty = useCallback3(() => {
1896
- if (!optionsRef.current.onSave) return;
1897
- setSaveState("dirty");
1898
- autosave?.();
1899
- }, [autosave]);
1900
- const [initialJson] = useState3(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
3219
+ const markDirty = useCallback3(
3220
+ (userEdit = true) => {
3221
+ if (!optionsRef.current.onSave) {
3222
+ if (userEdit) setSaveState("dirty");
3223
+ return;
3224
+ }
3225
+ setSaveState("dirty");
3226
+ autosave?.();
3227
+ },
3228
+ [autosave]
3229
+ );
3230
+ useEffect6(() => {
3231
+ if (saveState !== "saved" || options.onSave) return;
3232
+ const t = setTimeout(() => setSaveState((s) => s === "saved" ? "idle" : s), 2500);
3233
+ return () => clearTimeout(t);
3234
+ }, [saveState, options.onSave]);
3235
+ const [initialJson] = useState6(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
1901
3236
  const extensions = useMemo4(() => createDocumentExtensions(), []);
1902
- const onUpdateRef = useRef3(() => {
3237
+ const onUpdateRef = useRef7(() => {
1903
3238
  });
1904
3239
  onUpdateRef.current = (json) => {
1905
3240
  store.edit(documentId, json, instanceId);
@@ -1921,11 +3256,13 @@ function DocumentEditorImpl(props) {
1921
3256
  },
1922
3257
  onUpdate: ({ editor: ed }) => onUpdateRef.current(ed.getJSON())
1923
3258
  });
1924
- useEffect2(() => {
1925
- editor?.setEditable(editable);
3259
+ useEffect6(() => {
3260
+ if (!editor || editor.isEditable === editable) return;
3261
+ editor.setEditable(editable);
1926
3262
  }, [editor, editable]);
1927
- useEffect2(() => {
3263
+ useEffect6(() => {
1928
3264
  if (!parsed.json) return;
3265
+ if (isDocumentEcho(store, documentId, contentKey)) return;
1929
3266
  const outcome = store.receive({
1930
3267
  id: documentId,
1931
3268
  title: titleProp,
@@ -1935,7 +3272,7 @@ function DocumentEditorImpl(props) {
1935
3272
  });
1936
3273
  if (outcome === "applied" || outcome === "merged") {
1937
3274
  emitChange(outcome === "applied" ? "ai" : "merge");
1938
- markDirty();
3275
+ markDirty(false);
1939
3276
  } else if (outcome === "conflict") {
1940
3277
  const record = store.get(documentId);
1941
3278
  optionsRef.current.onConflict?.({
@@ -1945,7 +3282,7 @@ function DocumentEditorImpl(props) {
1945
3282
  });
1946
3283
  }
1947
3284
  }, [store, documentId, titleProp, parsed.json, documentType, status, emitChange, markDirty]);
1948
- useEffect2(() => {
3285
+ useEffect6(() => {
1949
3286
  if (!editor) return;
1950
3287
  const apply = () => {
1951
3288
  const record = store.get(documentId);
@@ -1962,7 +3299,7 @@ function DocumentEditorImpl(props) {
1962
3299
  apply();
1963
3300
  return store.subscribe(apply);
1964
3301
  }, [editor, store, documentId, instanceId]);
1965
- useEffect2(() => {
3302
+ useEffect6(() => {
1966
3303
  if (!editor || readyRef.current) return;
1967
3304
  const record = store.get(documentId);
1968
3305
  if (!record) return;
@@ -2006,67 +3343,115 @@ function DocumentEditorImpl(props) {
2006
3343
  };
2007
3344
  const formats = (exportFormats ?? [...ALL_EXPORT_FORMATS]).filter((f) => listDocumentExporters().includes(f));
2008
3345
  if (parsed.error && !store.get(documentId)) {
2009
- return /* @__PURE__ */ jsxs4("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
2010
- /* @__PURE__ */ jsx8("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
2011
- /* @__PURE__ */ jsxs4("details", { children: [
2012
- /* @__PURE__ */ jsx8("summary", { children: "Show raw content" }),
2013
- /* @__PURE__ */ jsx8("pre", { className: "june-doc__raw", children: contentKey })
3346
+ return /* @__PURE__ */ jsxs10("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
3347
+ /* @__PURE__ */ jsx13("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
3348
+ /* @__PURE__ */ jsxs10("details", { children: [
3349
+ /* @__PURE__ */ jsx13("summary", { children: "Show raw content" }),
3350
+ /* @__PURE__ */ jsx13("pre", { className: "june-doc__raw", children: contentKey })
2014
3351
  ] })
2015
3352
  ] });
2016
3353
  }
2017
- if (!editor) return /* @__PURE__ */ jsx8(DocumentDraft, { title, state: "loading" });
3354
+ if (!editor) return /* @__PURE__ */ jsx13(DocumentDraft, { title, state: "loading" });
2018
3355
  const isDraft = recordStatus === "draft";
2019
- return /* @__PURE__ */ jsxs4(
3356
+ const classes = ["june-doc", fullscreen ? "june-doc--fullscreen" : "", className ?? ""].filter(Boolean).join(" ");
3357
+ return /* @__PURE__ */ jsxs10(
2020
3358
  "div",
2021
3359
  {
2022
- className: className ? `june-doc ${className}` : "june-doc",
3360
+ className: classes,
2023
3361
  "data-theme": resolvedTheme,
2024
3362
  "data-status": recordStatus,
3363
+ role: fullscreen ? "dialog" : void 0,
3364
+ "aria-modal": fullscreen || void 0,
3365
+ "aria-label": fullscreen ? title : void 0,
2025
3366
  onKeyDown: (e) => {
2026
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s" && options.onSave) {
3367
+ const mod = e.ctrlKey || e.metaKey;
3368
+ if (mod && e.key.toLowerCase() === "s" && options.onSave) {
2027
3369
  e.preventDefault();
2028
3370
  void saveRef.current();
3371
+ } else if (mod && e.key.toLowerCase() === "f") {
3372
+ e.preventDefault();
3373
+ setFindOpen(true);
3374
+ } else if (e.key === "Escape" && fullscreen && !e.defaultPrevented) {
3375
+ setFullscreen(false);
2029
3376
  }
2030
3377
  },
2031
3378
  children: [
2032
- /* @__PURE__ */ jsxs4("div", { className: "june-doc__header", children: [
2033
- /* @__PURE__ */ jsx8(DocumentTitle, { value: title, disabled: !editable, onChange: handleTitle }),
2034
- isDraft ? /* @__PURE__ */ jsx8("span", { className: "june-doc__badge", children: "AI draft" }) : null
2035
- ] }),
2036
- pending ? /* @__PURE__ */ jsxs4("div", { className: "june-doc__banner", role: "status", children: [
2037
- /* @__PURE__ */ jsxs4("span", { children: [
3379
+ /* @__PURE__ */ jsx13(
3380
+ DocumentHeader,
3381
+ {
3382
+ title,
3383
+ editable,
3384
+ isDraft,
3385
+ fullscreen,
3386
+ onToggleFullscreen: () => setFullscreen((v) => !v),
3387
+ onTitle: handleTitle,
3388
+ exportFormats: formats,
3389
+ canSave: !!options.onSave,
3390
+ showSave: !!options.onSave || editable && saveState !== "idle",
3391
+ saveState,
3392
+ onExport: (f) => void handleExport(f),
3393
+ onCopy: () => void handleCopy(),
3394
+ onSave: () => void saveRef.current()
3395
+ }
3396
+ ),
3397
+ pending ? /* @__PURE__ */ jsxs10("div", { className: "june-doc__banner", role: "status", children: [
3398
+ /* @__PURE__ */ jsxs10("span", { children: [
2038
3399
  "The assistant suggested changes that overlap your edits (",
2039
3400
  pending.conflicts,
2040
3401
  " ",
2041
3402
  pending.conflicts === 1 ? "section" : "sections",
2042
3403
  ")."
2043
3404
  ] }),
2044
- /* @__PURE__ */ jsxs4("span", { className: "june-doc__banner-actions", children: [
2045
- /* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
2046
- /* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
2047
- /* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
3405
+ /* @__PURE__ */ jsxs10("span", { className: "june-doc__banner-actions", children: [
3406
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
3407
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
3408
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
2048
3409
  ] })
2049
3410
  ] }) : null,
2050
- /* @__PURE__ */ jsx8(
3411
+ /* @__PURE__ */ jsx13(
2051
3412
  DocumentToolbar,
2052
3413
  {
2053
3414
  editor,
2054
3415
  editable,
2055
- exportFormats: formats,
2056
- canSave: !!options.onSave,
2057
- saveState,
2058
- onExport: (f) => void handleExport(f),
2059
- onCopy: () => void handleCopy(),
2060
- onSave: () => void saveRef.current()
3416
+ expanded,
3417
+ onToggleExpanded: () => setExpanded((v) => !v),
3418
+ showMarks,
3419
+ onToggleMarks: () => setShowMarks((v) => !v),
3420
+ layout,
3421
+ onToggleLayout: () => setLayout((v) => v === "page" ? "fluid" : "page"),
3422
+ findOpen,
3423
+ onToggleFind: () => setFindOpen((v) => !v),
3424
+ onNotice: flash
2061
3425
  }
2062
3426
  ),
2063
- /* @__PURE__ */ jsx8(DocumentContent, { editor }),
2064
- /* @__PURE__ */ jsxs4("div", { className: "june-doc__footer", "aria-live": "polite", children: [
2065
- /* @__PURE__ */ jsxs4("span", { children: [
2066
- words,
2067
- " words"
3427
+ findOpen ? /* @__PURE__ */ jsx13(DocumentFindBar, { editor, editable, onClose: () => setFindOpen(false), onNotice: flash }) : null,
3428
+ editable ? /* @__PURE__ */ jsx13(DocumentBubbleMenu, { editor }) : null,
3429
+ /* @__PURE__ */ jsx13(DocumentContent, { editor, layout, zoom, showMarks, onMetrics: setMetrics }),
3430
+ /* @__PURE__ */ jsxs10("div", { className: "june-doc__footer", children: [
3431
+ /* @__PURE__ */ jsxs10("div", { className: "june-doc__stats", children: [
3432
+ layout === "page" ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
3433
+ /* @__PURE__ */ jsxs10("span", { children: [
3434
+ "Page ",
3435
+ metrics.page,
3436
+ " of ",
3437
+ metrics.pages
3438
+ ] }),
3439
+ /* @__PURE__ */ jsx13("span", { className: "june-doc__stats-sep", "aria-hidden": "true" })
3440
+ ] }) : null,
3441
+ /* @__PURE__ */ jsxs10("span", { children: [
3442
+ words,
3443
+ " words"
3444
+ ] })
2068
3445
  ] }),
2069
- /* @__PURE__ */ jsx8("span", { children: notice ?? SAVE_LABEL[saveState] })
3446
+ /* @__PURE__ */ jsx13("span", { className: "june-doc__status", "aria-live": "polite", "data-state": notice ? "notice" : saveState, children: notice ?? SAVE_LABEL[saveState] }),
3447
+ /* @__PURE__ */ jsxs10("div", { className: "june-doc__zoom", role: "group", "aria-label": "Zoom", children: [
3448
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost", "aria-label": "Zoom out", disabled: zoom <= ZOOM_STEPS[0], onClick: () => setZoom(stepZoom(zoom, -1)), children: /* @__PURE__ */ jsx13(Icon, { name: "minus", size: 14 }) }),
3449
+ /* @__PURE__ */ jsxs10("button", { type: "button", className: "june-doc__zoom-value", "aria-label": "Reset zoom", title: "Reset zoom", onClick: () => setZoom(1), children: [
3450
+ Math.round(zoom * 100),
3451
+ "%"
3452
+ ] }),
3453
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost", "aria-label": "Zoom in", disabled: zoom >= ZOOM_STEPS[ZOOM_STEPS.length - 1], onClick: () => setZoom(stepZoom(zoom, 1)), children: /* @__PURE__ */ jsx13(Icon, { name: "plus", size: 14 }) })
3454
+ ] })
2070
3455
  ] })
2071
3456
  ]
2072
3457
  }
@@ -2074,7 +3459,7 @@ function DocumentEditorImpl(props) {
2074
3459
  }
2075
3460
 
2076
3461
  // src/components/document-editor/document-component.tsx
2077
- import { jsx as jsx9 } from "react/jsx-runtime";
3462
+ import { jsx as jsx14 } from "react/jsx-runtime";
2078
3463
  var DOCUMENT_COMPONENT_DESCRIPTION = 'Custom component. An editable document. DocumentEditor(documentId, title, content, documentType?, status?, editable?, exportFormats?). Place it in the root container (Card or Stack). "content" is the full document body as one Markdown string. Reuse documentId when revising the same document. Use it only when the user asks to create or revise a document.';
2079
3464
  var documentProps = z.object({
2080
3465
  documentId: z.string().describe("Stable id. Reuse it when revising the same document."),
@@ -2088,12 +3473,12 @@ var documentProps = z.object({
2088
3473
  function DocumentEditor(props) {
2089
3474
  const { isStreaming } = useJuneRuntime();
2090
3475
  if (props.generating || isStreaming) {
2091
- return /* @__PURE__ */ jsx9(DocumentDraft, { title: props.title, state: "drafting" });
3476
+ return /* @__PURE__ */ jsx14(DocumentDraft, { title: props.title, state: "drafting" });
2092
3477
  }
2093
- return /* @__PURE__ */ jsx9(DocumentEditorImpl, { ...props });
3478
+ return /* @__PURE__ */ jsx14(DocumentEditorImpl, { ...props });
2094
3479
  }
2095
3480
  function OpenUIDocument({ props }) {
2096
- return /* @__PURE__ */ jsx9(
3481
+ return /* @__PURE__ */ jsx14(
2097
3482
  DocumentEditor,
2098
3483
  {
2099
3484
  documentId: props.documentId,
@@ -2113,11 +3498,15 @@ var JuneDocumentEditorComponent = defineComponent2({
2113
3498
  component: OpenUIDocument
2114
3499
  });
2115
3500
  function withDocumentEditor(library) {
2116
- return library.extend({
2117
- components: {
2118
- add: [{ component: JuneDocumentEditorComponent, slots: ["Card"] }]
2119
- }
2120
- });
3501
+ try {
3502
+ return library.extend({
3503
+ components: {
3504
+ add: [{ component: JuneDocumentEditorComponent, slots: ["Card"] }]
3505
+ }
3506
+ });
3507
+ } catch {
3508
+ return library.extend({ components: { add: [JuneDocumentEditorComponent] } });
3509
+ }
2121
3510
  }
2122
3511
 
2123
3512
  // src/components/document-editor/document-instructions.ts
@@ -2164,7 +3553,7 @@ function documentPromptOptions(options = {}) {
2164
3553
  var attr = (value) => value.replace(/"/g, "&quot;");
2165
3554
  async function serializeDocumentsForAgent(options = {}) {
2166
3555
  const store = options.store ?? getDefaultDocumentStore();
2167
- const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-C6ILFMRP.js");
3556
+ const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-RKD5SLXR.js");
2168
3557
  return store.list().filter((r) => !options.ids || options.ids.includes(r.id)).map(
2169
3558
  (r) => `<document id="${attr(r.id)}" title="${attr(r.title)}"${r.documentType ? ` type="${attr(r.documentType)}"` : ""}>
2170
3559
  ${docToMarkdown2(r.json)}
@@ -2177,9 +3566,9 @@ import { openuiLibrary } from "@openuidev/react-ui";
2177
3566
  var juneLibrary = withDocumentEditor(openuiLibrary);
2178
3567
 
2179
3568
  // src/core/JuneUI.tsx
2180
- import { jsx as jsx10 } from "react/jsx-runtime";
3569
+ import { jsx as jsx15 } from "react/jsx-runtime";
2181
3570
  function JuneUI({ library = juneLibrary, ...props }) {
2182
- return /* @__PURE__ */ jsx10(JuneRenderer, { library, ...props });
3571
+ return /* @__PURE__ */ jsx15(JuneRenderer, { library, ...props });
2183
3572
  }
2184
3573
  export {
2185
3574
  ALL_EXPORT_FORMATS,
@@ -2208,17 +3597,21 @@ export {
2208
3597
  getDefaultDocumentStore,
2209
3598
  getDocumentEditorInstructions,
2210
3599
  getDocumentEditorRules,
3600
+ isDocumentEcho,
2211
3601
  isJuneError,
2212
3602
  jsonFieldExtractor,
2213
3603
  juneLibrary,
2214
3604
  lightTheme,
2215
3605
  listDocumentExporters,
2216
3606
  mergeDocuments,
3607
+ readDocumentFromProgram,
2217
3608
  registerDocumentExporter,
3609
+ rememberDocumentEcho,
2218
3610
  runAction,
2219
3611
  serializeDocumentsForAgent,
2220
3612
  sseToText,
2221
3613
  toJuneError,
3614
+ updateDocumentInProgram,
2222
3615
  useDocumentOptions,
2223
3616
  useJuneRuntime,
2224
3617
  useJuneStream,