@june-ai/june-ui 0.2.5 → 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,24 +6,28 @@ import {
6
6
  docToHtml,
7
7
  docToMarkdown,
8
8
  emptyDoc,
9
+ getPageCount,
9
10
  getSearchState,
10
11
  gotoMatch,
11
12
  isJuneError,
12
13
  parseDocumentContent,
14
+ remeasurePagination,
13
15
  replaceAll,
14
16
  replaceCurrent,
15
17
  sanitizeHref,
18
+ setPaginationConfig,
16
19
  setSearch,
17
20
  slugify,
18
21
  toJuneError,
19
22
  wordCountOf
20
- } from "./chunk-CZE533PM.js";
23
+ } from "./chunk-VFZI3SEL.js";
21
24
 
22
25
  // src/renderer/renderer.tsx
23
26
  import { Renderer } from "@openuidev/react-lang";
24
27
  import {
25
28
  Fragment,
26
29
  useCallback,
30
+ useEffect,
27
31
  useMemo as useMemo2,
28
32
  useRef
29
33
  } from "react";
@@ -101,6 +105,316 @@ function useDocumentValue(store, id, selector) {
101
105
  return useSyncExternalStore(store.subscribe, read, read);
102
106
  }
103
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
+
104
418
  // src/core/context.ts
105
419
  import { createContext as createContext2, useContext as useContext2 } from "react";
106
420
  var JuneContext = createContext2({
@@ -659,14 +973,43 @@ function JuneRenderer(props) {
659
973
  onError,
660
974
  onJuneError,
661
975
  documents,
976
+ onResponseChange,
977
+ responseChangeDelay = 300,
662
978
  loadingFallback = null,
663
979
  emptyFallback = null,
664
980
  errorFallback,
665
981
  resetKeys,
666
982
  ...rendererProps
667
983
  } = props;
668
- const latest = useRef({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError });
669
- 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
+ }, []);
670
1013
  const handleAction = useCallback((event) => {
671
1014
  const l = latest.current;
672
1015
  l.onAction?.(event);
@@ -705,12 +1048,27 @@ function JuneRenderer(props) {
705
1048
  const documentOptions = useMemo2(
706
1049
  () => ({
707
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
+ },
708
1066
  onError: (error) => {
709
1067
  documents?.onError?.(error);
710
1068
  latest.current.onJuneError?.(error);
711
1069
  }
712
1070
  }),
713
- [documents]
1071
+ [documents, syncProgram]
714
1072
  );
715
1073
  const parts = useMemo2(
716
1074
  () => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
@@ -803,7 +1161,7 @@ function defineJuneAction(definition) {
803
1161
  }
804
1162
 
805
1163
  // src/streaming/use-june-stream.ts
806
- 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";
807
1165
 
808
1166
  // src/streaming/stream-handler.ts
809
1167
  function isReadableStream(source) {
@@ -1058,7 +1416,7 @@ function useJuneStream(options = {}) {
1058
1416
  clearTimer();
1059
1417
  setState(INITIAL);
1060
1418
  }, [clearTimer]);
1061
- useEffect(
1419
+ useEffect2(
1062
1420
  () => () => {
1063
1421
  runIdRef.current++;
1064
1422
  controllerRef.current?.abort();
@@ -1095,7 +1453,7 @@ function DocumentDraft({ title, words, state }) {
1095
1453
 
1096
1454
  // src/components/document-editor/document-editor.tsx
1097
1455
  import { useEditor } from "@tiptap/react";
1098
- import { useCallback as useCallback3, useEffect as useEffect5, useId, useMemo as useMemo4, useRef as useRef7, useState as useState6 } from "react";
1456
+ import { useCallback as useCallback3, useEffect as useEffect6, useId, useMemo as useMemo4, useRef as useRef7, useState as useState6 } from "react";
1099
1457
 
1100
1458
  // src/components/document-editor/document-bubble.tsx
1101
1459
  import { useEditorState as useEditorState2 } from "@tiptap/react";
@@ -1327,12 +1685,12 @@ function Icon({ name, size = 16 }) {
1327
1685
  }
1328
1686
 
1329
1687
  // src/components/document-editor/document-popover.tsx
1330
- import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
1688
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
1331
1689
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1332
1690
  function Dropdown({ trigger, children, align = "start", className, onOpen }) {
1333
1691
  const [open, setOpen] = useState2(false);
1334
1692
  const anchor = useRef3(null);
1335
- useEffect2(() => {
1693
+ useEffect3(() => {
1336
1694
  if (!open) return;
1337
1695
  const onDown = (e) => {
1338
1696
  if (anchor.current && !anchor.current.contains(e.target)) setOpen(false);
@@ -1952,60 +2310,116 @@ function DocumentBubbleMenu({ editor }) {
1952
2310
 
1953
2311
  // src/components/document-editor/document-content.tsx
1954
2312
  import { EditorContent } from "@tiptap/react";
1955
- import { useEffect as useEffect3, useRef as useRef5, useState as useState4 } from "react";
1956
- import { jsx as jsx9 } from "react/jsx-runtime";
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;
1957
2316
  var PAGE_HEIGHT = 1123;
1958
- var PAGE_PAD_Y = 96;
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
+ }
1959
2327
  function DocumentContent({ editor, layout = "page", zoom = 1, showMarks = false, onMetrics }) {
1960
2328
  const canvas = useRef5(null);
1961
2329
  const sheet = useRef5(null);
2330
+ const [avail, setAvail] = useState4(PAGE_WIDTH);
2331
+ const [sheetH, setSheetH] = useState4(0);
1962
2332
  const [pages, setPages] = useState4(1);
1963
2333
  const metricsRef = useRef5(onMetrics);
1964
2334
  metricsRef.current = onMetrics;
1965
2335
  const last = useRef5({ page: 0, pages: 0 });
1966
- useEffect3(() => {
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(() => {
1967
2341
  const c = canvas.current;
1968
- const p = sheet.current;
1969
- const prose = editor?.view.dom;
1970
- if (!c || !p || !prose) return;
1971
- const measure = () => {
1972
- const contentH = prose.getBoundingClientRect().height / zoom;
1973
- const total = layout === "page" ? Math.max(1, Math.ceil((contentH + PAGE_PAD_Y * 2) / PAGE_HEIGHT)) : 1;
1974
- const cRect = c.getBoundingClientRect();
1975
- const pRect = p.getBoundingClientRect();
1976
- const y = cRect.top + c.clientHeight * 0.35 - pRect.top;
1977
- const page = Math.min(total, Math.max(1, Math.floor(y / (PAGE_HEIGHT * zoom)) + 1));
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;
1978
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;
1979
2378
  if (last.current.page !== page || last.current.pages !== total) {
1980
2379
  last.current = { page, pages: total };
1981
2380
  metricsRef.current?.({ page, pages: total });
1982
2381
  }
1983
2382
  };
1984
- measure();
1985
- c.addEventListener("scroll", measure, { passive: true });
1986
- const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
1987
- ro?.observe(prose);
1988
- ro?.observe(c);
1989
- editor?.on("update", measure);
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);
1990
2390
  return () => {
1991
- c.removeEventListener("scroll", measure);
2391
+ editor.off("transaction", report);
2392
+ c.removeEventListener("scroll", report);
2393
+ prose.removeEventListener("load", onLoad, true);
1992
2394
  ro?.disconnect();
1993
- editor?.off("update", measure);
1994
2395
  };
1995
- }, [editor, layout, zoom]);
1996
- const sheetStyle = layout === "page" ? {
1997
- zoom,
1998
- minHeight: pages * PAGE_HEIGHT,
1999
- maxWidth: `calc(100% / ${zoom})`,
2000
- "--jd-page-h": `${PAGE_HEIGHT}px`
2001
- } : { zoom };
2002
- 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", { ref: sheet, className: "june-doc__sheet", style: sheetStyle, children: /* @__PURE__ */ jsx9(EditorContent, { editor }) }) });
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
+ ] }) }) });
2003
2417
  }
2004
2418
 
2005
2419
  // src/components/document-editor/document-findbar.tsx
2006
2420
  import { useEditorState as useEditorState3 } from "@tiptap/react";
2007
- import { useEffect as useEffect4, useRef as useRef6, useState as useState5 } from "react";
2008
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
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";
2009
2423
  function DocumentFindBar({ editor, editable, onClose, onNotice }) {
2010
2424
  const [term, setTerm] = useState5(() => {
2011
2425
  const { from, to, empty } = editor.state.selection;
@@ -2021,19 +2435,19 @@ function DocumentFindBar({ editor, editable, onClose, onNotice }) {
2021
2435
  return { count: s?.matches.length ?? 0, current: s?.current ?? -1 };
2022
2436
  }
2023
2437
  });
2024
- useEffect4(() => {
2438
+ useEffect5(() => {
2025
2439
  input.current?.focus();
2026
2440
  input.current?.select();
2027
2441
  }, []);
2028
- useEffect4(() => {
2442
+ useEffect5(() => {
2029
2443
  setSearch(editor, { term, caseSensitive, current: 0 });
2030
2444
  }, [editor, term, caseSensitive]);
2031
- useEffect4(() => () => {
2445
+ useEffect5(() => () => {
2032
2446
  if (!editor.isDestroyed) setSearch(editor, { term: "" });
2033
2447
  }, [editor]);
2034
2448
  const count = info?.count ?? 0;
2035
2449
  const current = info?.current ?? -1;
2036
- return /* @__PURE__ */ jsxs7(
2450
+ return /* @__PURE__ */ jsxs8(
2037
2451
  "div",
2038
2452
  {
2039
2453
  className: "june-doc__findbar",
@@ -2046,7 +2460,7 @@ function DocumentFindBar({ editor, editable, onClose, onNotice }) {
2046
2460
  }
2047
2461
  },
2048
2462
  children: [
2049
- /* @__PURE__ */ jsxs7("div", { className: "june-doc__find-field", children: [
2463
+ /* @__PURE__ */ jsxs8("div", { className: "june-doc__find-field", children: [
2050
2464
  /* @__PURE__ */ jsx10(Icon, { name: "search", size: 14 }),
2051
2465
  /* @__PURE__ */ jsx10(
2052
2466
  "input",
@@ -2082,7 +2496,7 @@ function DocumentFindBar({ editor, editable, onClose, onNotice }) {
2082
2496
  children: "Aa"
2083
2497
  }
2084
2498
  ),
2085
- editable ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
2499
+ editable ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2086
2500
  /* @__PURE__ */ jsx10("span", { className: "june-doc__find-sep", "aria-hidden": "true" }),
2087
2501
  /* @__PURE__ */ jsx10(
2088
2502
  "input",
@@ -2138,7 +2552,7 @@ function DocumentTitle({ value, disabled, onChange }) {
2138
2552
  }
2139
2553
 
2140
2554
  // src/components/document-editor/document-header.tsx
2141
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2555
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2142
2556
  var FORMATS = {
2143
2557
  docx: { label: "Word document", hint: ".docx", icon: "file" },
2144
2558
  pdf: { label: "PDF", hint: "Print / Save as PDF", icon: "file" },
@@ -2147,20 +2561,21 @@ var FORMATS = {
2147
2561
  txt: { label: "Plain text", hint: ".txt", icon: "file" }
2148
2562
  };
2149
2563
  function DocumentHeader(props) {
2150
- const { title, editable, isDraft, fullscreen, onToggleFullscreen, onTitle, exportFormats, canSave, saveState, onExport, onCopy, onSave } = props;
2151
- return /* @__PURE__ */ jsxs8("div", { className: "june-doc__header", children: [
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: [
2152
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 }) }),
2153
2567
  /* @__PURE__ */ jsx12(DocumentTitle, { value: title, disabled: !editable, onChange: onTitle }),
2154
- isDraft ? /* @__PURE__ */ jsxs8("span", { className: "june-doc__badge", children: [
2568
+ isDraft ? /* @__PURE__ */ jsxs9("span", { className: "june-doc__badge", children: [
2155
2569
  /* @__PURE__ */ jsx12(Icon, { name: "sparkles", size: 12 }),
2156
2570
  "AI draft"
2157
2571
  ] }) : null,
2158
- /* @__PURE__ */ jsxs8("div", { className: "june-doc__header-actions", children: [
2159
- canSave ? /* @__PURE__ */ jsxs8(
2572
+ /* @__PURE__ */ jsxs9("div", { className: "june-doc__header-actions", children: [
2573
+ showSave ? /* @__PURE__ */ jsxs9(
2160
2574
  "button",
2161
2575
  {
2162
2576
  type: "button",
2163
- className: "june-doc__btn june-doc__btn--outline",
2577
+ className: "june-doc__btn june-doc__btn--outline june-doc__save",
2578
+ "data-state": saveState,
2164
2579
  "aria-label": "Save",
2165
2580
  title: "Save (Ctrl+S)",
2166
2581
  disabled: saveState === "saving" || saveState === "saved" || saveState === "idle",
@@ -2176,7 +2591,7 @@ function DocumentHeader(props) {
2176
2591
  {
2177
2592
  align: "end",
2178
2593
  className: "june-doc__menu",
2179
- trigger: ({ open, toggle }) => /* @__PURE__ */ jsxs8(
2594
+ trigger: ({ open, toggle }) => /* @__PURE__ */ jsxs9(
2180
2595
  "button",
2181
2596
  {
2182
2597
  type: "button",
@@ -2191,10 +2606,10 @@ function DocumentHeader(props) {
2191
2606
  ]
2192
2607
  }
2193
2608
  ),
2194
- children: (close) => /* @__PURE__ */ jsxs8("div", { role: "menu", "aria-label": "Download options", children: [
2609
+ children: (close) => /* @__PURE__ */ jsxs9("div", { role: "menu", "aria-label": "Download options", children: [
2195
2610
  exportFormats.map((f) => {
2196
2611
  const meta = FORMATS[f] ?? { label: f.toUpperCase(), hint: "", icon: "file" };
2197
- return /* @__PURE__ */ jsxs8(
2612
+ return /* @__PURE__ */ jsxs9(
2198
2613
  "button",
2199
2614
  {
2200
2615
  type: "button",
@@ -2214,7 +2629,7 @@ function DocumentHeader(props) {
2214
2629
  );
2215
2630
  }),
2216
2631
  exportFormats.length ? /* @__PURE__ */ jsx12("div", { className: "june-doc__menu-sep", role: "separator" }) : null,
2217
- /* @__PURE__ */ jsxs8(
2632
+ /* @__PURE__ */ jsxs9(
2218
2633
  "button",
2219
2634
  {
2220
2635
  type: "button",
@@ -2265,7 +2680,7 @@ var textResult = (text, filename, mimeType) => ({
2265
2680
  blob: new Blob([text], { type: `${mimeType};charset=utf-8` })
2266
2681
  });
2267
2682
  var exportHtml = async (ctx) => {
2268
- const { docToHtml: docToHtml2 } = await import("./document-engine-IAY2T6B3.js");
2683
+ const { docToHtml: docToHtml2 } = await import("./document-engine-RKD5SLXR.js");
2269
2684
  return textResult(
2270
2685
  buildHtmlDocument(ctx.title, docToHtml2(ctx.json)),
2271
2686
  `${ctx.basename}.html`,
@@ -2273,7 +2688,7 @@ var exportHtml = async (ctx) => {
2273
2688
  );
2274
2689
  };
2275
2690
  var exportMarkdown = async (ctx) => {
2276
- const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-IAY2T6B3.js");
2691
+ const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-RKD5SLXR.js");
2277
2692
  return textResult(
2278
2693
  `# ${ctx.title}
2279
2694
 
@@ -2284,7 +2699,7 @@ ${docToMarkdown2(ctx.json)}
2284
2699
  );
2285
2700
  };
2286
2701
  var exportTxt = async (ctx) => {
2287
- const { docToText } = await import("./document-engine-IAY2T6B3.js");
2702
+ const { docToText } = await import("./document-engine-RKD5SLXR.js");
2288
2703
  return textResult(`${ctx.title}
2289
2704
 
2290
2705
  ${docToText(ctx.json)}
@@ -2606,7 +3021,7 @@ function printHtml(html) {
2606
3021
  });
2607
3022
  }
2608
3023
  var exportPdf = async (ctx) => {
2609
- const { docToHtml: docToHtml2 } = await import("./document-engine-IAY2T6B3.js");
3024
+ const { docToHtml: docToHtml2 } = await import("./document-engine-RKD5SLXR.js");
2610
3025
  await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
2611
3026
  return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
2612
3027
  };
@@ -2644,7 +3059,7 @@ function downloadResult(result) {
2644
3059
  setTimeout(() => URL.revokeObjectURL(url), 1e3);
2645
3060
  }
2646
3061
  async function copyDocumentToClipboard(doc) {
2647
- const { docToHtml: docToHtml2, docToText } = await import("./document-engine-IAY2T6B3.js");
3062
+ const { docToHtml: docToHtml2, docToText } = await import("./document-engine-RKD5SLXR.js");
2648
3063
  const html = docToHtml2(doc.json);
2649
3064
  const text = `${doc.title}
2650
3065
 
@@ -2666,221 +3081,6 @@ ${docToText(doc.json)}`;
2666
3081
  }
2667
3082
  }
2668
3083
 
2669
- // src/components/document-editor/document-merge.ts
2670
- var MAX_CELLS = 4e6;
2671
- function matchIndices(a, b) {
2672
- const n = a.length;
2673
- const m = b.length;
2674
- if (n * m > MAX_CELLS) return null;
2675
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
2676
- for (let i2 = n - 1; i2 >= 0; i2--) {
2677
- for (let j2 = m - 1; j2 >= 0; j2--) {
2678
- dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
2679
- }
2680
- }
2681
- const matches = /* @__PURE__ */ new Map();
2682
- let i = 0;
2683
- let j = 0;
2684
- while (i < n && j < m) {
2685
- if (a[i] === b[j]) {
2686
- matches.set(i, j);
2687
- i++;
2688
- j++;
2689
- } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
2690
- else j++;
2691
- }
2692
- return matches;
2693
- }
2694
- var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
2695
- function mergeBlocks(base, local, incoming) {
2696
- if (same(local, base)) return { blocks: incoming, conflicts: 0 };
2697
- if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
2698
- const ml = matchIndices(base, local);
2699
- const mn = matchIndices(base, incoming);
2700
- if (!ml || !mn) return { blocks: null, conflicts: 1 };
2701
- const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
2702
- const out = [];
2703
- let conflicts = 0;
2704
- let pb = -1;
2705
- let pl = -1;
2706
- let pn = -1;
2707
- const region = (bEnd, lEnd, nEnd) => {
2708
- const B = base.slice(pb + 1, bEnd);
2709
- const L = local.slice(pl + 1, lEnd);
2710
- const N = incoming.slice(pn + 1, nEnd);
2711
- if (B.length === L.length && L.length === N.length) {
2712
- for (let k = 0; k < B.length; k++) {
2713
- if (L[k] === B[k]) out.push(N[k]);
2714
- else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
2715
- else {
2716
- conflicts++;
2717
- out.push(L[k]);
2718
- }
2719
- }
2720
- return;
2721
- }
2722
- if (same(L, B)) out.push(...N);
2723
- else if (same(N, B) || same(L, N)) out.push(...L);
2724
- else {
2725
- conflicts++;
2726
- out.push(...L);
2727
- }
2728
- };
2729
- for (const a of anchors) {
2730
- const la = ml.get(a);
2731
- const na = mn.get(a);
2732
- region(a, la, na);
2733
- out.push(local[la]);
2734
- pb = a;
2735
- pl = la;
2736
- pn = na;
2737
- }
2738
- region(base.length, local.length, incoming.length);
2739
- return { blocks: out, conflicts };
2740
- }
2741
- function mergeDocuments(base, local, incoming) {
2742
- const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
2743
- const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
2744
- if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
2745
- if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
2746
- return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
2747
- }
2748
-
2749
- // src/components/document-editor/document-store.ts
2750
- var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
2751
- function createDocumentStore() {
2752
- const records = /* @__PURE__ */ new Map();
2753
- const listeners = /* @__PURE__ */ new Set();
2754
- const emit = () => listeners.forEach((listener) => listener());
2755
- const put = (record) => {
2756
- records.set(record.id, record);
2757
- emit();
2758
- };
2759
- const fresh2 = (doc) => ({
2760
- id: doc.id,
2761
- title: doc.title,
2762
- documentType: doc.documentType,
2763
- status: doc.status,
2764
- json: doc.json,
2765
- baseJson: doc.json,
2766
- baseTitle: doc.title,
2767
- lastIncomingKey: incomingKey(doc),
2768
- revision: 0,
2769
- origin: "external",
2770
- pending: null,
2771
- updatedAt: Date.now()
2772
- });
2773
- return {
2774
- get: (id) => records.get(id),
2775
- list: () => [...records.values()],
2776
- subscribe: (listener) => {
2777
- listeners.add(listener);
2778
- return () => {
2779
- listeners.delete(listener);
2780
- };
2781
- },
2782
- receive(incoming) {
2783
- const current = records.get(incoming.id);
2784
- if (!current) {
2785
- put(fresh2(incoming));
2786
- return "created";
2787
- }
2788
- const key = incomingKey(incoming);
2789
- if (current.lastIncomingKey === key) return "unchanged";
2790
- const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
2791
- const userEditedTitle = current.title !== current.baseTitle;
2792
- const title = userEditedTitle ? current.title : incoming.title;
2793
- const meta = {
2794
- documentType: incoming.documentType ?? current.documentType,
2795
- status: incoming.status ?? current.status
2796
- };
2797
- if (!userEditedBody) {
2798
- put({
2799
- ...current,
2800
- ...meta,
2801
- title,
2802
- baseTitle: incoming.title,
2803
- json: incoming.json,
2804
- baseJson: incoming.json,
2805
- lastIncomingKey: key,
2806
- revision: current.revision + 1,
2807
- origin: "external",
2808
- pending: null,
2809
- updatedAt: Date.now()
2810
- });
2811
- return "applied";
2812
- }
2813
- const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
2814
- if (conflicts === 0) {
2815
- put({
2816
- ...current,
2817
- ...meta,
2818
- title,
2819
- baseTitle: incoming.title,
2820
- json: doc,
2821
- baseJson: incoming.json,
2822
- lastIncomingKey: key,
2823
- revision: current.revision + 1,
2824
- origin: "external",
2825
- pending: null,
2826
- updatedAt: Date.now()
2827
- });
2828
- return "merged";
2829
- }
2830
- put({
2831
- ...current,
2832
- ...meta,
2833
- lastIncomingKey: key,
2834
- pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
2835
- });
2836
- return "conflict";
2837
- },
2838
- hydrate(doc) {
2839
- put(fresh2(doc));
2840
- },
2841
- edit(id, json, origin) {
2842
- const current = records.get(id);
2843
- if (!current || docKey(current.json) === docKey(json)) return;
2844
- put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
2845
- },
2846
- setTitle(id, title) {
2847
- const current = records.get(id);
2848
- if (!current || current.title === title) return;
2849
- put({ ...current, title, updatedAt: Date.now() });
2850
- },
2851
- resolve(id, choice) {
2852
- const current = records.get(id);
2853
- const pending = current?.pending;
2854
- if (!current || !pending) return;
2855
- if (choice === "keep") {
2856
- put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
2857
- return;
2858
- }
2859
- const json = choice === "merge" ? pending.merged : pending.json;
2860
- const title = choice === "merge" ? current.title : pending.title;
2861
- put({
2862
- ...current,
2863
- title,
2864
- json,
2865
- baseJson: pending.json,
2866
- baseTitle: pending.title,
2867
- revision: current.revision + 1,
2868
- origin: "external",
2869
- pending: null,
2870
- updatedAt: Date.now()
2871
- });
2872
- },
2873
- remove(id) {
2874
- if (records.delete(id)) emit();
2875
- }
2876
- };
2877
- }
2878
- var defaultStore = null;
2879
- function getDefaultDocumentStore() {
2880
- if (!defaultStore) defaultStore = createDocumentStore();
2881
- return defaultStore;
2882
- }
2883
-
2884
3084
  // src/components/document-editor/document-types.ts
2885
3085
  var ALL_EXPORT_FORMATS = [
2886
3086
  "html",
@@ -2891,7 +3091,7 @@ var ALL_EXPORT_FORMATS = [
2891
3091
  ];
2892
3092
 
2893
3093
  // src/components/document-editor/document-editor.tsx
2894
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3094
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2895
3095
  var snapshotOf = (r) => ({
2896
3096
  documentId: r.id,
2897
3097
  title: r.title,
@@ -2937,7 +3137,7 @@ function DocumentEditorImpl(props) {
2937
3137
  return { json: null, error: toJuneError(e, "DOCUMENT_ERROR") };
2938
3138
  }
2939
3139
  }, [contentKey]);
2940
- useEffect5(() => {
3140
+ useEffect6(() => {
2941
3141
  if (parsed.error) optionsRef.current.onError?.(parsed.error);
2942
3142
  }, [parsed.error]);
2943
3143
  const title = useDocumentValue(store, documentId, (r) => r?.title ?? titleProp);
@@ -2953,7 +3153,7 @@ function DocumentEditorImpl(props) {
2953
3153
  const [findOpen, setFindOpen] = useState6(false);
2954
3154
  const [zoom, setZoom] = useState6(1);
2955
3155
  const [metrics, setMetrics] = useState6({ page: 1, pages: 1 });
2956
- useEffect5(() => {
3156
+ useEffect6(() => {
2957
3157
  if (!fullscreen || typeof document === "undefined") return;
2958
3158
  const prev = document.body.style.overflow;
2959
3159
  document.body.style.overflow = "hidden";
@@ -2967,7 +3167,7 @@ function DocumentEditorImpl(props) {
2967
3167
  const flash = useCallback3((message) => {
2968
3168
  setNotice(message);
2969
3169
  }, []);
2970
- useEffect5(() => {
3170
+ useEffect6(() => {
2971
3171
  if (!notice) return;
2972
3172
  const t = setTimeout(() => setNotice(null), 3e3);
2973
3173
  return () => clearTimeout(t);
@@ -2985,12 +3185,17 @@ function DocumentEditorImpl(props) {
2985
3185
  const delay = options.autosaveDelay ?? 1500;
2986
3186
  return delay > 0 ? debounce(() => void saveRef.current(), delay) : null;
2987
3187
  }, [options.autosaveDelay]);
2988
- useEffect5(() => () => autosave?.flush(), [autosave]);
3188
+ useEffect6(() => () => autosave?.flush(), [autosave]);
2989
3189
  saveRef.current = async () => {
2990
3190
  const record = store.get(documentId);
2991
3191
  const onSave = optionsRef.current.onSave;
2992
- if (!record || !onSave) return;
3192
+ if (!record) return;
2993
3193
  const key = JSON.stringify([record.title, docKey(record.json)]);
3194
+ if (!onSave) {
3195
+ lastSavedKey.current = key;
3196
+ setSaveState("saved");
3197
+ return;
3198
+ }
2994
3199
  if (key === lastSavedKey.current) {
2995
3200
  setSaveState("saved");
2996
3201
  return;
@@ -3011,11 +3216,22 @@ function DocumentEditorImpl(props) {
3011
3216
  optionsRef.current.onError?.(toJuneError(error, "DOCUMENT_ERROR", "Saving the document failed"));
3012
3217
  }
3013
3218
  };
3014
- const markDirty = useCallback3(() => {
3015
- if (!optionsRef.current.onSave) return;
3016
- setSaveState("dirty");
3017
- autosave?.();
3018
- }, [autosave]);
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]);
3019
3235
  const [initialJson] = useState6(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
3020
3236
  const extensions = useMemo4(() => createDocumentExtensions(), []);
3021
3237
  const onUpdateRef = useRef7(() => {
@@ -3040,11 +3256,13 @@ function DocumentEditorImpl(props) {
3040
3256
  },
3041
3257
  onUpdate: ({ editor: ed }) => onUpdateRef.current(ed.getJSON())
3042
3258
  });
3043
- useEffect5(() => {
3044
- editor?.setEditable(editable);
3259
+ useEffect6(() => {
3260
+ if (!editor || editor.isEditable === editable) return;
3261
+ editor.setEditable(editable);
3045
3262
  }, [editor, editable]);
3046
- useEffect5(() => {
3263
+ useEffect6(() => {
3047
3264
  if (!parsed.json) return;
3265
+ if (isDocumentEcho(store, documentId, contentKey)) return;
3048
3266
  const outcome = store.receive({
3049
3267
  id: documentId,
3050
3268
  title: titleProp,
@@ -3054,7 +3272,7 @@ function DocumentEditorImpl(props) {
3054
3272
  });
3055
3273
  if (outcome === "applied" || outcome === "merged") {
3056
3274
  emitChange(outcome === "applied" ? "ai" : "merge");
3057
- markDirty();
3275
+ markDirty(false);
3058
3276
  } else if (outcome === "conflict") {
3059
3277
  const record = store.get(documentId);
3060
3278
  optionsRef.current.onConflict?.({
@@ -3064,7 +3282,7 @@ function DocumentEditorImpl(props) {
3064
3282
  });
3065
3283
  }
3066
3284
  }, [store, documentId, titleProp, parsed.json, documentType, status, emitChange, markDirty]);
3067
- useEffect5(() => {
3285
+ useEffect6(() => {
3068
3286
  if (!editor) return;
3069
3287
  const apply = () => {
3070
3288
  const record = store.get(documentId);
@@ -3081,7 +3299,7 @@ function DocumentEditorImpl(props) {
3081
3299
  apply();
3082
3300
  return store.subscribe(apply);
3083
3301
  }, [editor, store, documentId, instanceId]);
3084
- useEffect5(() => {
3302
+ useEffect6(() => {
3085
3303
  if (!editor || readyRef.current) return;
3086
3304
  const record = store.get(documentId);
3087
3305
  if (!record) return;
@@ -3125,9 +3343,9 @@ function DocumentEditorImpl(props) {
3125
3343
  };
3126
3344
  const formats = (exportFormats ?? [...ALL_EXPORT_FORMATS]).filter((f) => listDocumentExporters().includes(f));
3127
3345
  if (parsed.error && !store.get(documentId)) {
3128
- return /* @__PURE__ */ jsxs9("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
3346
+ return /* @__PURE__ */ jsxs10("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
3129
3347
  /* @__PURE__ */ jsx13("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
3130
- /* @__PURE__ */ jsxs9("details", { children: [
3348
+ /* @__PURE__ */ jsxs10("details", { children: [
3131
3349
  /* @__PURE__ */ jsx13("summary", { children: "Show raw content" }),
3132
3350
  /* @__PURE__ */ jsx13("pre", { className: "june-doc__raw", children: contentKey })
3133
3351
  ] })
@@ -3136,7 +3354,7 @@ function DocumentEditorImpl(props) {
3136
3354
  if (!editor) return /* @__PURE__ */ jsx13(DocumentDraft, { title, state: "loading" });
3137
3355
  const isDraft = recordStatus === "draft";
3138
3356
  const classes = ["june-doc", fullscreen ? "june-doc--fullscreen" : "", className ?? ""].filter(Boolean).join(" ");
3139
- return /* @__PURE__ */ jsxs9(
3357
+ return /* @__PURE__ */ jsxs10(
3140
3358
  "div",
3141
3359
  {
3142
3360
  className: classes,
@@ -3169,21 +3387,22 @@ function DocumentEditorImpl(props) {
3169
3387
  onTitle: handleTitle,
3170
3388
  exportFormats: formats,
3171
3389
  canSave: !!options.onSave,
3390
+ showSave: !!options.onSave || editable && saveState !== "idle",
3172
3391
  saveState,
3173
3392
  onExport: (f) => void handleExport(f),
3174
3393
  onCopy: () => void handleCopy(),
3175
3394
  onSave: () => void saveRef.current()
3176
3395
  }
3177
3396
  ),
3178
- pending ? /* @__PURE__ */ jsxs9("div", { className: "june-doc__banner", role: "status", children: [
3179
- /* @__PURE__ */ jsxs9("span", { children: [
3397
+ pending ? /* @__PURE__ */ jsxs10("div", { className: "june-doc__banner", role: "status", children: [
3398
+ /* @__PURE__ */ jsxs10("span", { children: [
3180
3399
  "The assistant suggested changes that overlap your edits (",
3181
3400
  pending.conflicts,
3182
3401
  " ",
3183
3402
  pending.conflicts === 1 ? "section" : "sections",
3184
3403
  ")."
3185
3404
  ] }),
3186
- /* @__PURE__ */ jsxs9("span", { className: "june-doc__banner-actions", children: [
3405
+ /* @__PURE__ */ jsxs10("span", { className: "june-doc__banner-actions", children: [
3187
3406
  /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
3188
3407
  /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
3189
3408
  /* @__PURE__ */ jsx13("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
@@ -3208,10 +3427,10 @@ function DocumentEditorImpl(props) {
3208
3427
  findOpen ? /* @__PURE__ */ jsx13(DocumentFindBar, { editor, editable, onClose: () => setFindOpen(false), onNotice: flash }) : null,
3209
3428
  editable ? /* @__PURE__ */ jsx13(DocumentBubbleMenu, { editor }) : null,
3210
3429
  /* @__PURE__ */ jsx13(DocumentContent, { editor, layout, zoom, showMarks, onMetrics: setMetrics }),
3211
- /* @__PURE__ */ jsxs9("div", { className: "june-doc__footer", children: [
3212
- /* @__PURE__ */ jsxs9("div", { className: "june-doc__stats", children: [
3213
- layout === "page" ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
3214
- /* @__PURE__ */ jsxs9("span", { children: [
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: [
3215
3434
  "Page ",
3216
3435
  metrics.page,
3217
3436
  " of ",
@@ -3219,15 +3438,15 @@ function DocumentEditorImpl(props) {
3219
3438
  ] }),
3220
3439
  /* @__PURE__ */ jsx13("span", { className: "june-doc__stats-sep", "aria-hidden": "true" })
3221
3440
  ] }) : null,
3222
- /* @__PURE__ */ jsxs9("span", { children: [
3441
+ /* @__PURE__ */ jsxs10("span", { children: [
3223
3442
  words,
3224
3443
  " words"
3225
3444
  ] })
3226
3445
  ] }),
3227
3446
  /* @__PURE__ */ jsx13("span", { className: "june-doc__status", "aria-live": "polite", "data-state": notice ? "notice" : saveState, children: notice ?? SAVE_LABEL[saveState] }),
3228
- /* @__PURE__ */ jsxs9("div", { className: "june-doc__zoom", role: "group", "aria-label": "Zoom", children: [
3447
+ /* @__PURE__ */ jsxs10("div", { className: "june-doc__zoom", role: "group", "aria-label": "Zoom", children: [
3229
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 }) }),
3230
- /* @__PURE__ */ jsxs9("button", { type: "button", className: "june-doc__zoom-value", "aria-label": "Reset zoom", title: "Reset zoom", onClick: () => setZoom(1), children: [
3449
+ /* @__PURE__ */ jsxs10("button", { type: "button", className: "june-doc__zoom-value", "aria-label": "Reset zoom", title: "Reset zoom", onClick: () => setZoom(1), children: [
3231
3450
  Math.round(zoom * 100),
3232
3451
  "%"
3233
3452
  ] }),
@@ -3334,7 +3553,7 @@ function documentPromptOptions(options = {}) {
3334
3553
  var attr = (value) => value.replace(/"/g, "&quot;");
3335
3554
  async function serializeDocumentsForAgent(options = {}) {
3336
3555
  const store = options.store ?? getDefaultDocumentStore();
3337
- const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-IAY2T6B3.js");
3556
+ const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-RKD5SLXR.js");
3338
3557
  return store.list().filter((r) => !options.ids || options.ids.includes(r.id)).map(
3339
3558
  (r) => `<document id="${attr(r.id)}" title="${attr(r.title)}"${r.documentType ? ` type="${attr(r.documentType)}"` : ""}>
3340
3559
  ${docToMarkdown2(r.json)}
@@ -3378,17 +3597,21 @@ export {
3378
3597
  getDefaultDocumentStore,
3379
3598
  getDocumentEditorInstructions,
3380
3599
  getDocumentEditorRules,
3600
+ isDocumentEcho,
3381
3601
  isJuneError,
3382
3602
  jsonFieldExtractor,
3383
3603
  juneLibrary,
3384
3604
  lightTheme,
3385
3605
  listDocumentExporters,
3386
3606
  mergeDocuments,
3607
+ readDocumentFromProgram,
3387
3608
  registerDocumentExporter,
3609
+ rememberDocumentEcho,
3388
3610
  runAction,
3389
3611
  serializeDocumentsForAgent,
3390
3612
  sseToText,
3391
3613
  toJuneError,
3614
+ updateDocumentInProgram,
3392
3615
  useDocumentOptions,
3393
3616
  useJuneRuntime,
3394
3617
  useJuneStream,