@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.cjs CHANGED
@@ -58,30 +58,6 @@ var init_errors = __esm({
58
58
  }
59
59
  });
60
60
 
61
- // src/components/document-editor/document-extensions.ts
62
- function createDocumentExtensions() {
63
- return [
64
- import_starter_kit.default.configure({
65
- heading: { levels: [1, 2, 3] },
66
- // Tiptap v3 StarterKit already includes Link and Underline
67
- link: { openOnClick: false, autolink: true, defaultProtocol: "https" }
68
- }),
69
- import_extension_text_align.default.configure({ types: ["heading", "paragraph"] }),
70
- import_extension_table.TableKit,
71
- import_markdown.Markdown
72
- ];
73
- }
74
- var import_extension_table, import_extension_text_align, import_markdown, import_starter_kit;
75
- var init_document_extensions = __esm({
76
- "src/components/document-editor/document-extensions.ts"() {
77
- "use strict";
78
- import_extension_table = require("@tiptap/extension-table");
79
- import_extension_text_align = __toESM(require("@tiptap/extension-text-align"), 1);
80
- import_markdown = require("@tiptap/markdown");
81
- import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
82
- }
83
- });
84
-
85
61
  // src/components/document-editor/document-utils.ts
86
62
  function isDocJSON(value) {
87
63
  if (!value || typeof value !== "object") return false;
@@ -169,6 +145,380 @@ var init_document_utils = __esm({
169
145
  }
170
146
  });
171
147
 
148
+ // src/components/document-editor/document-pagination.ts
149
+ function gapWidget(height) {
150
+ return () => {
151
+ const el = document.createElement("div");
152
+ el.className = "june-doc__page-gap";
153
+ el.contentEditable = "false";
154
+ el.setAttribute("aria-hidden", "true");
155
+ el.style.height = `${height}px`;
156
+ return el;
157
+ };
158
+ }
159
+ function build(doc, breaks) {
160
+ return import_view.DecorationSet.create(
161
+ doc,
162
+ breaks.map(
163
+ (b) => import_view.Decoration.widget(b.pos, gapWidget(b.height), {
164
+ side: -1,
165
+ ignoreSelection: true,
166
+ key: `jd-gap-${b.pos}-${Math.round(b.height)}`
167
+ })
168
+ )
169
+ );
170
+ }
171
+ function collectUnits(doc) {
172
+ const units = [];
173
+ const walk = (parent, start, inheritedBreak) => {
174
+ parent.forEach((child, offset, index) => {
175
+ const pos = start + offset;
176
+ const breakPos = index === 0 && inheritedBreak !== null ? inheritedBreak : pos;
177
+ if (SPLITTABLE.has(child.type.name) && child.childCount > 0) {
178
+ walk(child, pos + 1, breakPos);
179
+ } else {
180
+ units.push({ pos, breakPos, node: child });
181
+ }
182
+ });
183
+ };
184
+ walk(doc, 0, null);
185
+ return units;
186
+ }
187
+ function measure(view, cfg) {
188
+ const prose = view.dom;
189
+ const proseRect = prose.getBoundingClientRect();
190
+ const scale = prose.offsetWidth ? proseRect.width / prose.offsetWidth : 1;
191
+ if (!scale || !proseRect.width) return { breaks: [], pages: 1 };
192
+ const gaps = [...prose.querySelectorAll(".june-doc__page-gap")].map((g) => {
193
+ const r = g.getBoundingClientRect();
194
+ return { top: (r.top - proseRect.top) / scale, height: r.height / scale };
195
+ });
196
+ const gapsAbove = (y) => gaps.reduce((sum, g) => g.top <= y + 0.5 ? sum + g.height : sum, 0);
197
+ const contentH = Math.max(80, cfg.pageHeight - cfg.padTop - cfg.padBottom);
198
+ const stride = cfg.pageHeight + cfg.gap;
199
+ const breaks = /* @__PURE__ */ new Map();
200
+ let pageStart = 0;
201
+ let added = 0;
202
+ let force = false;
203
+ for (const unit of collectUnits(view.state.doc)) {
204
+ const el = view.nodeDOM(unit.pos);
205
+ if (!(el instanceof HTMLElement)) continue;
206
+ const r = el.getBoundingClientRect();
207
+ const rawTop = (r.top - proseRect.top) / scale;
208
+ const natural = rawTop - gapsAbove(rawTop);
209
+ const h = r.height / scale;
210
+ let top = natural + added;
211
+ let bottom = top + h;
212
+ const overflows = bottom > pageStart + contentH + 0.5 && top > pageStart + 1;
213
+ if (force || overflows) {
214
+ const target = pageStart + stride;
215
+ if (top < target) {
216
+ const spacer = target - top;
217
+ breaks.set(unit.breakPos, (breaks.get(unit.breakPos) ?? 0) + spacer);
218
+ added += spacer;
219
+ top = target;
220
+ bottom = top + h;
221
+ }
222
+ pageStart = target;
223
+ while (top >= pageStart + stride) pageStart += stride;
224
+ force = false;
225
+ }
226
+ while (bottom > pageStart + contentH + 0.5) pageStart += stride;
227
+ if (unit.node.type.name === "pageBreak") force = true;
228
+ }
229
+ const pages = Math.max(1, Math.round(pageStart / stride) + 1 + (force ? 1 : 0));
230
+ return {
231
+ breaks: [...breaks].map(([pos, height]) => ({ pos, height: Math.round(height * 10) / 10 })).sort((a, b) => a.pos - b.pos),
232
+ pages
233
+ };
234
+ }
235
+ function setPaginationConfig(editor, cfg) {
236
+ if (editor.isDestroyed) return;
237
+ const prev = configs.get(editor.view);
238
+ configs.set(editor.view, cfg);
239
+ if (!prev || prev.enabled !== cfg.enabled || prev.pageHeight !== cfg.pageHeight || prev.gap !== cfg.gap || prev.padTop !== cfg.padTop || prev.padBottom !== cfg.padBottom) {
240
+ remeasurePagination(editor);
241
+ }
242
+ }
243
+ function remeasurePagination(editor) {
244
+ if (editor.isDestroyed) return;
245
+ const s = paginationKey.getState(editor.state);
246
+ if (!s) return;
247
+ editor.view.dispatch(editor.state.tr.setMeta(paginationKey, { remeasure: true }).setMeta("addToHistory", false));
248
+ }
249
+ function getPageCount(editor) {
250
+ return editor && paginationKey.getState(editor.state)?.pages || 1;
251
+ }
252
+ var import_core, import_state, import_view, paginationKey, configs, SPLITTABLE, same2, DocumentPagination;
253
+ var init_document_pagination = __esm({
254
+ "src/components/document-editor/document-pagination.ts"() {
255
+ "use strict";
256
+ import_core = require("@tiptap/core");
257
+ import_state = require("@tiptap/pm/state");
258
+ import_view = require("@tiptap/pm/view");
259
+ paginationKey = new import_state.PluginKey("juneDocPagination");
260
+ configs = /* @__PURE__ */ new WeakMap();
261
+ SPLITTABLE = /* @__PURE__ */ new Set(["bulletList", "orderedList", "listItem", "blockquote"]);
262
+ same2 = (a, b) => a.length === b.length && a.every((x, i) => x.pos === b[i].pos && Math.abs(x.height - b[i].height) < 1);
263
+ DocumentPagination = import_core.Extension.create({
264
+ name: "juneDocPagination",
265
+ addProseMirrorPlugins() {
266
+ return [
267
+ new import_state.Plugin({
268
+ key: paginationKey,
269
+ state: {
270
+ init: () => ({ breaks: [], pages: 1, decorations: import_view.DecorationSet.empty, epoch: 0 }),
271
+ apply(tr, prev, _old, state) {
272
+ const meta = tr.getMeta(paginationKey);
273
+ if (meta?.remeasure) return { ...prev, epoch: prev.epoch + 1 };
274
+ if (meta?.breaks) {
275
+ return { ...prev, breaks: meta.breaks, pages: meta.pages ?? prev.pages, decorations: build(state.doc, meta.breaks) };
276
+ }
277
+ if (tr.docChanged) {
278
+ const breaks = prev.breaks.map((b) => ({ ...b, pos: tr.mapping.map(b.pos, -1) })).filter((b) => b.pos >= 0 && b.pos <= state.doc.content.size);
279
+ return { ...prev, breaks, decorations: prev.decorations.map(tr.mapping, tr.doc) };
280
+ }
281
+ return prev;
282
+ }
283
+ },
284
+ props: {
285
+ decorations: (state) => paginationKey.getState(state)?.decorations
286
+ },
287
+ view(view) {
288
+ let frame = null;
289
+ let rounds = 0;
290
+ const schedule = (reset) => {
291
+ if (reset) rounds = 0;
292
+ if (frame !== null || typeof window === "undefined") return;
293
+ const raf = window.requestAnimationFrame ?? ((cb) => window.setTimeout(() => cb(0), 16));
294
+ frame = raf(() => {
295
+ frame = null;
296
+ if (view.isDestroyed) return;
297
+ const cfg = configs.get(view);
298
+ const current = paginationKey.getState(view.state);
299
+ if (!current) return;
300
+ const next = cfg?.enabled ? measure(view, cfg) : { breaks: [], pages: 1 };
301
+ if (same2(next.breaks, current.breaks) && next.pages === current.pages) return;
302
+ if (++rounds > 4) return;
303
+ view.dispatch(view.state.tr.setMeta(paginationKey, next).setMeta("addToHistory", false));
304
+ });
305
+ };
306
+ schedule(true);
307
+ return {
308
+ update(v, prevState) {
309
+ const pg = paginationKey.getState(v.state);
310
+ const prevPg = paginationKey.getState(prevState);
311
+ if (v.state.doc !== prevState.doc || pg?.epoch !== prevPg?.epoch) schedule(true);
312
+ else if (pg !== prevPg) schedule(false);
313
+ },
314
+ destroy() {
315
+ frame = null;
316
+ }
317
+ };
318
+ }
319
+ })
320
+ ];
321
+ }
322
+ });
323
+ }
324
+ });
325
+
326
+ // src/components/document-editor/document-search.ts
327
+ function findMatches(doc, term, caseSensitive) {
328
+ if (!term) return [];
329
+ const needle = caseSensitive ? term : term.toLowerCase();
330
+ const out = [];
331
+ doc.descendants((node, pos) => {
332
+ if (!node.isTextblock) return true;
333
+ let text = "";
334
+ const map = [];
335
+ node.forEach((child, offset) => {
336
+ const start = pos + 1 + offset;
337
+ if (child.isText && child.text) {
338
+ for (let i = 0; i < child.text.length; i++) map.push(start + i);
339
+ text += child.text;
340
+ } else {
341
+ map.push(start);
342
+ text += "\uFFFC";
343
+ }
344
+ });
345
+ const hay = caseSensitive ? text : text.toLowerCase();
346
+ let idx = hay.indexOf(needle);
347
+ while (idx !== -1) {
348
+ out.push({ from: map[idx], to: map[idx + needle.length - 1] + 1 });
349
+ idx = hay.indexOf(needle, idx + needle.length);
350
+ }
351
+ return false;
352
+ });
353
+ return out;
354
+ }
355
+ function build2(doc, term, caseSensitive, current) {
356
+ const matches = findMatches(doc, term, caseSensitive);
357
+ const active = matches.length ? Math.min(Math.max(current, 0), matches.length - 1) : -1;
358
+ const decorations = import_view2.DecorationSet.create(
359
+ doc,
360
+ matches.map(
361
+ (m, i) => import_view2.Decoration.inline(m.from, m.to, {
362
+ class: i === active ? "june-doc__match june-doc__match--active" : "june-doc__match"
363
+ })
364
+ )
365
+ );
366
+ return { term, caseSensitive, current: active, matches, decorations };
367
+ }
368
+ function getSearchState(editor) {
369
+ return editor ? searchKey.getState(editor.state) : void 0;
370
+ }
371
+ function setSearch(editor, meta) {
372
+ editor.view.dispatch(editor.state.tr.setMeta(searchKey, meta));
373
+ }
374
+ function gotoMatch(editor, step) {
375
+ const s = getSearchState(editor);
376
+ if (!s || !s.matches.length) return;
377
+ const n = s.matches.length;
378
+ const current = s.current < 0 ? 0 : (s.current + step + n) % n;
379
+ setSearch(editor, { current });
380
+ const m = s.matches[current];
381
+ editor.chain().setTextSelection(m).scrollIntoView().run();
382
+ }
383
+ function replaceCurrent(editor, replacement) {
384
+ const s = getSearchState(editor);
385
+ if (!s || s.current < 0) return;
386
+ const m = s.matches[s.current];
387
+ editor.chain().command(({ tr }) => {
388
+ if (replacement) tr.insertText(replacement, m.from, m.to);
389
+ else tr.delete(m.from, m.to);
390
+ return true;
391
+ }).run();
392
+ gotoMatch(editor, 0);
393
+ }
394
+ function replaceAll(editor, replacement) {
395
+ const s = getSearchState(editor);
396
+ if (!s || !s.matches.length) return 0;
397
+ const matches = [...s.matches].reverse();
398
+ editor.chain().command(({ tr }) => {
399
+ for (const m of matches) {
400
+ if (replacement) tr.insertText(replacement, m.from, m.to);
401
+ else tr.delete(m.from, m.to);
402
+ }
403
+ return true;
404
+ }).run();
405
+ return matches.length;
406
+ }
407
+ var import_core2, import_state2, import_view2, searchKey, DocumentSearch;
408
+ var init_document_search = __esm({
409
+ "src/components/document-editor/document-search.ts"() {
410
+ "use strict";
411
+ import_core2 = require("@tiptap/core");
412
+ import_state2 = require("@tiptap/pm/state");
413
+ import_view2 = require("@tiptap/pm/view");
414
+ searchKey = new import_state2.PluginKey("juneDocSearch");
415
+ DocumentSearch = import_core2.Extension.create({
416
+ name: "juneDocSearch",
417
+ addProseMirrorPlugins() {
418
+ return [
419
+ new import_state2.Plugin({
420
+ key: searchKey,
421
+ state: {
422
+ init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations: import_view2.DecorationSet.empty }),
423
+ apply(tr, prev, _old, state) {
424
+ const meta = tr.getMeta(searchKey);
425
+ if (!meta && !tr.docChanged) return prev;
426
+ if (!meta && !prev.term) return prev;
427
+ return build2(
428
+ state.doc,
429
+ meta?.term ?? prev.term,
430
+ meta?.caseSensitive ?? prev.caseSensitive,
431
+ meta?.current ?? prev.current
432
+ );
433
+ }
434
+ },
435
+ props: {
436
+ decorations: (state) => searchKey.getState(state)?.decorations
437
+ }
438
+ })
439
+ ];
440
+ }
441
+ });
442
+ }
443
+ });
444
+
445
+ // src/components/document-editor/document-extensions.ts
446
+ function createDocumentExtensions() {
447
+ return [
448
+ import_starter_kit.default.configure({
449
+ heading: { levels: [1, 2, 3] },
450
+ // Tiptap v3 StarterKit already includes Link and Underline
451
+ link: { openOnClick: false, autolink: true, defaultProtocol: "https" }
452
+ }),
453
+ import_extension_text_align.default.configure({ types: ["heading", "paragraph"] }),
454
+ import_extension_table.TableKit,
455
+ import_extension_text_style.TextStyle,
456
+ import_extension_text_style.Color,
457
+ import_extension_text_style.FontFamily,
458
+ import_extension_text_style.FontSize,
459
+ import_extension_highlight.default.configure({ multicolor: true }),
460
+ import_extension_subscript.default,
461
+ import_extension_superscript.default,
462
+ import_extension_image.default.configure({
463
+ inline: false,
464
+ allowBase64: true,
465
+ resize: { enabled: true, minWidth: 48, minHeight: 32, alwaysPreserveAspectRatio: true }
466
+ }),
467
+ PageBreak,
468
+ DocumentSearch,
469
+ DocumentPagination,
470
+ // inactive until the live editor sets a page geometry
471
+ import_markdown.Markdown
472
+ ];
473
+ }
474
+ var import_core3, import_extension_highlight, import_extension_image, import_extension_subscript, import_extension_superscript, import_extension_table, import_extension_text_align, import_extension_text_style, import_markdown, import_starter_kit, PageBreak;
475
+ var init_document_extensions = __esm({
476
+ "src/components/document-editor/document-extensions.ts"() {
477
+ "use strict";
478
+ import_core3 = require("@tiptap/core");
479
+ import_extension_highlight = __toESM(require("@tiptap/extension-highlight"), 1);
480
+ import_extension_image = __toESM(require("@tiptap/extension-image"), 1);
481
+ import_extension_subscript = __toESM(require("@tiptap/extension-subscript"), 1);
482
+ import_extension_superscript = __toESM(require("@tiptap/extension-superscript"), 1);
483
+ import_extension_table = require("@tiptap/extension-table");
484
+ import_extension_text_align = __toESM(require("@tiptap/extension-text-align"), 1);
485
+ import_extension_text_style = require("@tiptap/extension-text-style");
486
+ import_markdown = require("@tiptap/markdown");
487
+ import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
488
+ init_document_pagination();
489
+ init_document_search();
490
+ PageBreak = import_core3.Node.create({
491
+ name: "pageBreak",
492
+ group: "block",
493
+ atom: true,
494
+ selectable: true,
495
+ parseHTML() {
496
+ return [{ tag: "div[data-page-break]" }];
497
+ },
498
+ renderHTML({ HTMLAttributes }) {
499
+ return [
500
+ "div",
501
+ (0, import_core3.mergeAttributes)(HTMLAttributes, {
502
+ "data-page-break": "true",
503
+ class: "june-doc__page-break",
504
+ style: "break-after: page; page-break-after: always;"
505
+ })
506
+ ];
507
+ },
508
+ // Markdown has no page break; a thematic break is the closest stable equivalent.
509
+ renderMarkdown: () => "---",
510
+ addCommands() {
511
+ return {
512
+ setPageBreak: () => ({ chain }) => chain().insertContent([{ type: this.name }, { type: "paragraph" }]).run()
513
+ };
514
+ },
515
+ addKeyboardShortcuts() {
516
+ return { "Mod-Enter": () => this.editor.commands.setPageBreak() };
517
+ }
518
+ });
519
+ }
520
+ });
521
+
172
522
  // src/components/document-editor/document-engine.ts
173
523
  var document_engine_exports = {};
174
524
  __export(document_engine_exports, {
@@ -179,7 +529,7 @@ __export(document_engine_exports, {
179
529
  });
180
530
  function scratchEditor() {
181
531
  if (!scratch || scratch.isDestroyed) {
182
- scratch = new import_core.Editor({ extensions: createDocumentExtensions(), content: "" });
532
+ scratch = new import_core4.Editor({ extensions: createDocumentExtensions(), content: "" });
183
533
  }
184
534
  return scratch;
185
535
  }
@@ -205,11 +555,11 @@ function load(doc) {
205
555
  editor.commands.setContent(doc, { emitUpdate: false });
206
556
  return editor;
207
557
  }
208
- var import_core, scratch, docToHtml, docToMarkdown, docToText;
558
+ var import_core4, scratch, docToHtml, docToMarkdown, docToText;
209
559
  var init_document_engine = __esm({
210
560
  "src/components/document-editor/document-engine.ts"() {
211
561
  "use strict";
212
- import_core = require("@tiptap/core");
562
+ import_core4 = require("@tiptap/core");
213
563
  init_errors();
214
564
  init_document_extensions();
215
565
  init_document_utils();
@@ -249,17 +599,21 @@ __export(index_exports, {
249
599
  getDefaultDocumentStore: () => getDefaultDocumentStore,
250
600
  getDocumentEditorInstructions: () => getDocumentEditorInstructions,
251
601
  getDocumentEditorRules: () => getDocumentEditorRules,
602
+ isDocumentEcho: () => isDocumentEcho,
252
603
  isJuneError: () => isJuneError,
253
604
  jsonFieldExtractor: () => jsonFieldExtractor,
254
605
  juneLibrary: () => juneLibrary,
255
606
  lightTheme: () => lightTheme,
256
607
  listDocumentExporters: () => listDocumentExporters,
257
608
  mergeDocuments: () => mergeDocuments,
609
+ readDocumentFromProgram: () => readDocumentFromProgram,
258
610
  registerDocumentExporter: () => registerDocumentExporter,
611
+ rememberDocumentEcho: () => rememberDocumentEcho,
259
612
  runAction: () => runAction,
260
613
  serializeDocumentsForAgent: () => serializeDocumentsForAgent,
261
614
  sseToText: () => sseToText,
262
615
  toJuneError: () => toJuneError,
616
+ updateDocumentInProgram: () => updateDocumentInProgram,
263
617
  useDocumentOptions: () => useDocumentOptions,
264
618
  useJuneRuntime: () => useJuneRuntime,
265
619
  useJuneStream: () => useJuneStream,
@@ -347,80 +701,392 @@ function useDocumentValue(store, id, selector) {
347
701
  return (0, import_react.useSyncExternalStore)(store.subscribe, read, read);
348
702
  }
349
703
 
350
- // src/core/context.ts
351
- var import_react2 = require("react");
352
- var JuneContext = (0, import_react2.createContext)({
353
- mode: "light",
354
- theme: "light"
355
- });
356
- function useJuneTheme() {
357
- return (0, import_react2.useContext)(JuneContext);
358
- }
359
- var JuneRuntimeContext = (0, import_react2.createContext)({ isStreaming: false });
360
- function useJuneRuntime() {
361
- return (0, import_react2.useContext)(JuneRuntimeContext);
704
+ // src/components/document-editor/document-merge.ts
705
+ init_document_utils();
706
+ var MAX_CELLS = 4e6;
707
+ function matchIndices(a, b) {
708
+ const n = a.length;
709
+ const m = b.length;
710
+ if (n * m > MAX_CELLS) return null;
711
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
712
+ for (let i2 = n - 1; i2 >= 0; i2--) {
713
+ for (let j2 = m - 1; j2 >= 0; j2--) {
714
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
715
+ }
716
+ }
717
+ const matches = /* @__PURE__ */ new Map();
718
+ let i = 0;
719
+ let j = 0;
720
+ while (i < n && j < m) {
721
+ if (a[i] === b[j]) {
722
+ matches.set(i, j);
723
+ i++;
724
+ j++;
725
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
726
+ else j++;
727
+ }
728
+ return matches;
362
729
  }
363
-
364
- // src/renderer/renderer.tsx
365
- init_errors();
366
-
367
- // src/themes/theme-provider.tsx
368
- var import_react3 = require("react");
369
-
370
- // src/themes/dark.ts
371
- var darkTheme = {
372
- "--june-foreground": "#f3f4f6",
373
- "--june-muted": "#9ca3af",
374
- "--june-border": "#1f2937",
375
- "--june-accent": "#818cf8",
376
- "--june-danger": "#f87171"
377
- };
378
-
379
- // src/themes/light.ts
380
- var lightTheme = {
381
- "--june-foreground": "#111827",
382
- "--june-muted": "#6b7280",
383
- "--june-border": "#e5e7eb",
384
- "--june-accent": "#4f46e5",
385
- "--june-danger": "#b91c1c"
386
- };
387
-
388
- // src/themes/theme-provider.tsx
389
- var import_jsx_runtime = require("react/jsx-runtime");
390
- var QUERY = "(prefers-color-scheme: dark)";
391
- function subscribe(onChange) {
392
- if (typeof window === "undefined" || !window.matchMedia) return () => {
730
+ var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
731
+ function mergeBlocks(base, local, incoming) {
732
+ if (same(local, base)) return { blocks: incoming, conflicts: 0 };
733
+ if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
734
+ const ml = matchIndices(base, local);
735
+ const mn = matchIndices(base, incoming);
736
+ if (!ml || !mn) return { blocks: null, conflicts: 1 };
737
+ const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
738
+ const out = [];
739
+ let conflicts = 0;
740
+ let pb = -1;
741
+ let pl = -1;
742
+ let pn = -1;
743
+ const region = (bEnd, lEnd, nEnd) => {
744
+ const B = base.slice(pb + 1, bEnd);
745
+ const L = local.slice(pl + 1, lEnd);
746
+ const N = incoming.slice(pn + 1, nEnd);
747
+ if (B.length === L.length && L.length === N.length) {
748
+ for (let k = 0; k < B.length; k++) {
749
+ if (L[k] === B[k]) out.push(N[k]);
750
+ else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
751
+ else {
752
+ conflicts++;
753
+ out.push(L[k]);
754
+ }
755
+ }
756
+ return;
757
+ }
758
+ if (same(L, B)) out.push(...N);
759
+ else if (same(N, B) || same(L, N)) out.push(...L);
760
+ else {
761
+ conflicts++;
762
+ out.push(...L);
763
+ }
393
764
  };
394
- const mq = window.matchMedia(QUERY);
395
- mq.addEventListener("change", onChange);
396
- return () => mq.removeEventListener("change", onChange);
397
- }
398
- function getSnapshot() {
399
- if (typeof window === "undefined") return "light";
400
- return window.matchMedia?.(QUERY)?.matches ? "dark" : "light";
401
- }
402
- function getServerSnapshot() {
403
- return "light";
765
+ for (const a of anchors) {
766
+ const la = ml.get(a);
767
+ const na = mn.get(a);
768
+ region(a, la, na);
769
+ out.push(local[la]);
770
+ pb = a;
771
+ pl = la;
772
+ pn = na;
773
+ }
774
+ region(base.length, local.length, incoming.length);
775
+ return { blocks: out, conflicts };
404
776
  }
405
- function useResolvedTheme(mode) {
406
- const system = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot);
407
- return mode === "system" ? system : mode;
777
+ function mergeDocuments(base, local, incoming) {
778
+ const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
779
+ const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
780
+ if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
781
+ if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
782
+ return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
408
783
  }
409
- function JuneThemeProvider({
410
- mode,
411
- vars,
412
- className,
413
- style,
414
- children
415
- }) {
416
- const explicit = mode !== void 0;
417
- const resolved = useResolvedTheme(mode ?? "system");
418
- const mergedStyle = (0, import_react3.useMemo)(() => {
419
- if (!explicit && !vars && !style) return void 0;
420
- const base = explicit ? resolved === "dark" ? darkTheme : lightTheme : void 0;
421
- return {
422
- ...base,
423
- ...vars,
784
+
785
+ // src/components/document-editor/document-store.ts
786
+ init_document_utils();
787
+ var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
788
+ function createDocumentStore() {
789
+ const records = /* @__PURE__ */ new Map();
790
+ const listeners = /* @__PURE__ */ new Set();
791
+ const emit = () => listeners.forEach((listener) => listener());
792
+ const put = (record) => {
793
+ records.set(record.id, record);
794
+ emit();
795
+ };
796
+ const fresh2 = (doc) => ({
797
+ id: doc.id,
798
+ title: doc.title,
799
+ documentType: doc.documentType,
800
+ status: doc.status,
801
+ json: doc.json,
802
+ baseJson: doc.json,
803
+ baseTitle: doc.title,
804
+ lastIncomingKey: incomingKey(doc),
805
+ revision: 0,
806
+ origin: "external",
807
+ pending: null,
808
+ updatedAt: Date.now()
809
+ });
810
+ return {
811
+ get: (id) => records.get(id),
812
+ list: () => [...records.values()],
813
+ subscribe: (listener) => {
814
+ listeners.add(listener);
815
+ return () => {
816
+ listeners.delete(listener);
817
+ };
818
+ },
819
+ receive(incoming) {
820
+ const current = records.get(incoming.id);
821
+ if (!current) {
822
+ put(fresh2(incoming));
823
+ return "created";
824
+ }
825
+ const key = incomingKey(incoming);
826
+ if (current.lastIncomingKey === key) return "unchanged";
827
+ const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
828
+ const userEditedTitle = current.title !== current.baseTitle;
829
+ const title = userEditedTitle ? current.title : incoming.title;
830
+ const meta = {
831
+ documentType: incoming.documentType ?? current.documentType,
832
+ status: incoming.status ?? current.status
833
+ };
834
+ if (!userEditedBody) {
835
+ put({
836
+ ...current,
837
+ ...meta,
838
+ title,
839
+ baseTitle: incoming.title,
840
+ json: incoming.json,
841
+ baseJson: incoming.json,
842
+ lastIncomingKey: key,
843
+ revision: current.revision + 1,
844
+ origin: "external",
845
+ pending: null,
846
+ updatedAt: Date.now()
847
+ });
848
+ return "applied";
849
+ }
850
+ const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
851
+ if (conflicts === 0) {
852
+ put({
853
+ ...current,
854
+ ...meta,
855
+ title,
856
+ baseTitle: incoming.title,
857
+ json: doc,
858
+ baseJson: incoming.json,
859
+ lastIncomingKey: key,
860
+ revision: current.revision + 1,
861
+ origin: "external",
862
+ pending: null,
863
+ updatedAt: Date.now()
864
+ });
865
+ return "merged";
866
+ }
867
+ put({
868
+ ...current,
869
+ ...meta,
870
+ lastIncomingKey: key,
871
+ pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
872
+ });
873
+ return "conflict";
874
+ },
875
+ hydrate(doc) {
876
+ put(fresh2(doc));
877
+ },
878
+ edit(id, json, origin) {
879
+ const current = records.get(id);
880
+ if (!current || docKey(current.json) === docKey(json)) return;
881
+ put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
882
+ },
883
+ setTitle(id, title) {
884
+ const current = records.get(id);
885
+ if (!current || current.title === title) return;
886
+ put({ ...current, title, updatedAt: Date.now() });
887
+ },
888
+ resolve(id, choice) {
889
+ const current = records.get(id);
890
+ const pending = current?.pending;
891
+ if (!current || !pending) return;
892
+ if (choice === "keep") {
893
+ put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
894
+ return;
895
+ }
896
+ const json = choice === "merge" ? pending.merged : pending.json;
897
+ const title = choice === "merge" ? current.title : pending.title;
898
+ put({
899
+ ...current,
900
+ title,
901
+ json,
902
+ baseJson: pending.json,
903
+ baseTitle: pending.title,
904
+ revision: current.revision + 1,
905
+ origin: "external",
906
+ pending: null,
907
+ updatedAt: Date.now()
908
+ });
909
+ },
910
+ remove(id) {
911
+ if (records.delete(id)) emit();
912
+ }
913
+ };
914
+ }
915
+ var echoes = /* @__PURE__ */ new WeakMap();
916
+ var ECHO_MEMORY = 8;
917
+ function rememberDocumentEcho(store, id, content) {
918
+ let byId = echoes.get(store);
919
+ if (!byId) echoes.set(store, byId = /* @__PURE__ */ new Map());
920
+ const list2 = byId.get(id) ?? [];
921
+ list2.push(content);
922
+ if (list2.length > ECHO_MEMORY) list2.shift();
923
+ byId.set(id, list2);
924
+ }
925
+ function isDocumentEcho(store, id, content) {
926
+ return !!content && !!echoes.get(store)?.get(id)?.includes(content);
927
+ }
928
+ var defaultStore = null;
929
+ function getDefaultDocumentStore() {
930
+ if (!defaultStore) defaultStore = createDocumentStore();
931
+ return defaultStore;
932
+ }
933
+
934
+ // src/utils/update-document-program.ts
935
+ function readString(src, i) {
936
+ if (src[i] !== '"') return null;
937
+ let j = i + 1;
938
+ let value = "";
939
+ while (j < src.length) {
940
+ const ch = src[j];
941
+ if (ch === '"') return { start: i, end: j + 1, value };
942
+ if (ch === "\\") {
943
+ const next = src[j + 1];
944
+ if (next === void 0) return null;
945
+ if (next === "u" && /^[0-9a-fA-F]{4}$/.test(src.slice(j + 2, j + 6))) {
946
+ value += String.fromCharCode(parseInt(src.slice(j + 2, j + 6), 16));
947
+ j += 6;
948
+ continue;
949
+ }
950
+ value += next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next === "b" ? "\b" : next === "f" ? "\f" : next;
951
+ j += 2;
952
+ continue;
953
+ }
954
+ value += ch;
955
+ j++;
956
+ }
957
+ return null;
958
+ }
959
+ var skipSpace = (src, i) => {
960
+ while (i < src.length && /\s/.test(src[i])) i++;
961
+ return i;
962
+ };
963
+ var toLangString = (value) => JSON.stringify(value);
964
+ function updateDocumentInProgram(program, documentId, update, componentName = "DocumentEditor") {
965
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
966
+ let match;
967
+ while (match = re.exec(program)) {
968
+ const args = [];
969
+ let i = match.index + match[0].length;
970
+ for (let n = 0; n < 3; n++) {
971
+ i = skipSpace(program, i);
972
+ const tok = readString(program, i);
973
+ if (!tok) break;
974
+ args.push(tok);
975
+ i = skipSpace(program, tok.end);
976
+ if (program[i] === ",") i++;
977
+ else break;
978
+ }
979
+ if (args[0]?.value !== documentId) continue;
980
+ const edits = [];
981
+ if (update.title !== void 0 && args[1] && args[1].value !== update.title) {
982
+ edits.push({ start: args[1].start, end: args[1].end, text: toLangString(update.title) });
983
+ }
984
+ if (update.content !== void 0 && args[2] && args[2].value !== update.content) {
985
+ edits.push({ start: args[2].start, end: args[2].end, text: toLangString(update.content) });
986
+ }
987
+ if (!args[2] && update.content !== void 0) return null;
988
+ let out = program;
989
+ for (const e of edits.sort((a, b) => b.start - a.start)) {
990
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
991
+ }
992
+ return out;
993
+ }
994
+ return null;
995
+ }
996
+ function readDocumentFromProgram(program, documentId, componentName = "DocumentEditor") {
997
+ const re = new RegExp(`\\b${componentName}\\s*\\(`, "g");
998
+ let match;
999
+ while (match = re.exec(program)) {
1000
+ const args = [];
1001
+ let i = match.index + match[0].length;
1002
+ for (let n = 0; n < 3; n++) {
1003
+ i = skipSpace(program, i);
1004
+ const tok = readString(program, i);
1005
+ if (!tok) break;
1006
+ args.push(tok);
1007
+ i = skipSpace(program, tok.end);
1008
+ if (program[i] === ",") i++;
1009
+ else break;
1010
+ }
1011
+ if (args[0]?.value === documentId && args[1] && args[2]) return { title: args[1].value, content: args[2].value };
1012
+ }
1013
+ return null;
1014
+ }
1015
+
1016
+ // src/core/context.ts
1017
+ var import_react2 = require("react");
1018
+ var JuneContext = (0, import_react2.createContext)({
1019
+ mode: "light",
1020
+ theme: "light"
1021
+ });
1022
+ function useJuneTheme() {
1023
+ return (0, import_react2.useContext)(JuneContext);
1024
+ }
1025
+ var JuneRuntimeContext = (0, import_react2.createContext)({ isStreaming: false });
1026
+ function useJuneRuntime() {
1027
+ return (0, import_react2.useContext)(JuneRuntimeContext);
1028
+ }
1029
+
1030
+ // src/renderer/renderer.tsx
1031
+ init_errors();
1032
+
1033
+ // src/themes/theme-provider.tsx
1034
+ var import_react3 = require("react");
1035
+
1036
+ // src/themes/dark.ts
1037
+ var darkTheme = {
1038
+ "--june-foreground": "#f3f4f6",
1039
+ "--june-muted": "#9ca3af",
1040
+ "--june-border": "#1f2937",
1041
+ "--june-accent": "#818cf8",
1042
+ "--june-danger": "#f87171"
1043
+ };
1044
+
1045
+ // src/themes/light.ts
1046
+ var lightTheme = {
1047
+ "--june-foreground": "#111827",
1048
+ "--june-muted": "#6b7280",
1049
+ "--june-border": "#e5e7eb",
1050
+ "--june-accent": "#4f46e5",
1051
+ "--june-danger": "#b91c1c"
1052
+ };
1053
+
1054
+ // src/themes/theme-provider.tsx
1055
+ var import_jsx_runtime = require("react/jsx-runtime");
1056
+ var QUERY = "(prefers-color-scheme: dark)";
1057
+ function subscribe(onChange) {
1058
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
1059
+ };
1060
+ const mq = window.matchMedia(QUERY);
1061
+ mq.addEventListener("change", onChange);
1062
+ return () => mq.removeEventListener("change", onChange);
1063
+ }
1064
+ function getSnapshot() {
1065
+ if (typeof window === "undefined") return "light";
1066
+ return window.matchMedia?.(QUERY)?.matches ? "dark" : "light";
1067
+ }
1068
+ function getServerSnapshot() {
1069
+ return "light";
1070
+ }
1071
+ function useResolvedTheme(mode) {
1072
+ const system = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot);
1073
+ return mode === "system" ? system : mode;
1074
+ }
1075
+ function JuneThemeProvider({
1076
+ mode,
1077
+ vars,
1078
+ className,
1079
+ style,
1080
+ children
1081
+ }) {
1082
+ const explicit = mode !== void 0;
1083
+ const resolved = useResolvedTheme(mode ?? "system");
1084
+ const mergedStyle = (0, import_react3.useMemo)(() => {
1085
+ if (!explicit && !vars && !style) return void 0;
1086
+ const base = explicit ? resolved === "dark" ? darkTheme : lightTheme : void 0;
1087
+ return {
1088
+ ...base,
1089
+ ...vars,
424
1090
  ...explicit ? { colorScheme: resolved } : {},
425
1091
  ...style
426
1092
  };
@@ -905,14 +1571,43 @@ function JuneRenderer(props) {
905
1571
  onError,
906
1572
  onJuneError,
907
1573
  documents,
1574
+ onResponseChange,
1575
+ responseChangeDelay = 300,
908
1576
  loadingFallback = null,
909
1577
  emptyFallback = null,
910
1578
  errorFallback,
911
1579
  resetKeys,
912
1580
  ...rendererProps
913
1581
  } = props;
914
- const latest = (0, import_react5.useRef)({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError });
915
- latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError };
1582
+ const latest = (0, import_react5.useRef)({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response });
1583
+ latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError, onResponseChange, responseChangeDelay, response };
1584
+ const syncTimers = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1585
+ (0, import_react5.useEffect)(() => {
1586
+ const timers = syncTimers.current;
1587
+ return () => {
1588
+ timers.forEach(clearTimeout);
1589
+ timers.clear();
1590
+ };
1591
+ }, []);
1592
+ const syncProgram = (0, import_react5.useCallback)(async (change, store) => {
1593
+ const l = latest.current;
1594
+ if (!l.onResponseChange || typeof l.response !== "string") return;
1595
+ const record = store.get(change.documentId);
1596
+ const title = record?.title ?? change.title;
1597
+ const json = record?.json ?? change.json;
1598
+ try {
1599
+ const { docToMarkdown: docToMarkdown2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
1600
+ const markdown = docToMarkdown2(json);
1601
+ const program = latest.current.response;
1602
+ if (typeof program !== "string") return;
1603
+ const next = updateDocumentInProgram(program, change.documentId, { title, content: markdown });
1604
+ if (next === null || next === program) return;
1605
+ rememberDocumentEcho(store, change.documentId, markdown);
1606
+ latest.current.onResponseChange?.(next, { documentId: change.documentId, title, markdown, source: change.source });
1607
+ } catch (error) {
1608
+ latest.current.onJuneError?.(toJuneError(error, "DOCUMENT_ERROR", "Updating the program failed"));
1609
+ }
1610
+ }, []);
916
1611
  const handleAction = (0, import_react5.useCallback)((event) => {
917
1612
  const l = latest.current;
918
1613
  l.onAction?.(event);
@@ -951,12 +1646,27 @@ function JuneRenderer(props) {
951
1646
  const documentOptions = (0, import_react5.useMemo)(
952
1647
  () => ({
953
1648
  ...documents,
1649
+ onDocumentChange: (change) => {
1650
+ documents?.onDocumentChange?.(change);
1651
+ if (!latest.current.onResponseChange || change.source === "ai") return;
1652
+ const store = documents?.store ?? getDefaultDocumentStore();
1653
+ const timers = syncTimers.current;
1654
+ const prev = timers.get(change.documentId);
1655
+ if (prev) clearTimeout(prev);
1656
+ timers.set(
1657
+ change.documentId,
1658
+ setTimeout(() => {
1659
+ timers.delete(change.documentId);
1660
+ void syncProgram(change, store);
1661
+ }, Math.max(0, latest.current.responseChangeDelay))
1662
+ );
1663
+ },
954
1664
  onError: (error) => {
955
1665
  documents?.onError?.(error);
956
1666
  latest.current.onJuneError?.(error);
957
1667
  }
958
1668
  }),
959
- [documents]
1669
+ [documents, syncProgram]
960
1670
  );
961
1671
  const parts = (0, import_react5.useMemo)(
962
1672
  () => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
@@ -1353,25 +2063,1231 @@ function DocumentDraft({ title, words, state }) {
1353
2063
  }
1354
2064
 
1355
2065
  // src/components/document-editor/document-editor.tsx
1356
- var import_react10 = require("@tiptap/react");
1357
- var import_react11 = require("react");
2066
+ var import_react15 = require("@tiptap/react");
2067
+ var import_react16 = require("react");
1358
2068
  init_errors();
1359
2069
 
1360
- // src/components/document-editor/document-content.tsx
1361
- var import_react7 = require("@tiptap/react");
2070
+ // src/components/document-editor/document-bubble.tsx
2071
+ var import_react10 = require("@tiptap/react");
2072
+ var import_menus = require("@tiptap/react/menus");
2073
+
2074
+ // src/components/document-editor/document-toolbar.tsx
2075
+ var import_react8 = require("@tiptap/react");
2076
+ var import_react9 = require("react");
2077
+
2078
+ // src/components/document-editor/document-icons.tsx
1362
2079
  var import_jsx_runtime5 = require("react/jsx-runtime");
1363
- function DocumentContent({ editor }) {
1364
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "june-doc__content", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react7.EditorContent, { editor }) });
2080
+ var PATHS = {
2081
+ undo: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2082
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 14 4 9l5-5" }),
2083
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
2084
+ ] }),
2085
+ redo: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2086
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m15 14 5-5-5-5" }),
2087
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M20 9H9.5a5.5 5.5 0 0 0 0 11H13" })
2088
+ ] }),
2089
+ bold: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" }),
2090
+ italic: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2091
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "19", x2: "10", y1: "4", y2: "4" }),
2092
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "14", x2: "5", y1: "20", y2: "20" }),
2093
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "15", x2: "9", y1: "4", y2: "20" })
2094
+ ] }),
2095
+ underline: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2096
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 4v6a6 6 0 0 0 12 0V4" }),
2097
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "4", x2: "20", y1: "20", y2: "20" })
2098
+ ] }),
2099
+ strike: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2100
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M16 4H9a3 3 0 0 0-2.83 4" }),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M14 12a4 4 0 0 1 0 8H6" }),
2102
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "4", x2: "20", y1: "12", y2: "12" })
2103
+ ] }),
2104
+ code: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2105
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m16 18 6-6-6-6" }),
2106
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m8 6-6 6 6 6" })
2107
+ ] }),
2108
+ codeBlock: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2109
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2110
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m10 9-3 3 3 3" }),
2111
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m14 15 3-3-3-3" })
2112
+ ] }),
2113
+ superscript: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2114
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m4 19 8-8" }),
2115
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m12 19-8-8" }),
2116
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" })
2117
+ ] }),
2118
+ subscript: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2119
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m4 5 8 8" }),
2120
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m12 5-8 8" }),
2121
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" })
2122
+ ] }),
2123
+ alignLeft: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2124
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M15 12H3" }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M17 18H3" }),
2126
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 6H3" })
2127
+ ] }),
2128
+ alignCenter: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2129
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M17 12H7" }),
2130
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 18H5" }),
2131
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 6H3" })
2132
+ ] }),
2133
+ alignRight: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2134
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 12H9" }),
2135
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 18H7" }),
2136
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 6H3" })
2137
+ ] }),
2138
+ alignJustify: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2139
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 12h18" }),
2140
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 18h18" }),
2141
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 6h18" })
2142
+ ] }),
2143
+ list: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2144
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 12h.01" }),
2145
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 18h.01" }),
2146
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 6h.01" }),
2147
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 12h13" }),
2148
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 18h13" }),
2149
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 6h13" })
2150
+ ] }),
2151
+ listOrdered: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2152
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M10 12h11" }),
2153
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M10 18h11" }),
2154
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M10 6h11" }),
2155
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M4 10h2" }),
2156
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M4 6h1v4" }),
2157
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })
2158
+ ] }),
2159
+ outdent: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2160
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 12H11" }),
2161
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 18H11" }),
2162
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 6H11" }),
2163
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m7 8-4 4 4 4" })
2164
+ ] }),
2165
+ indent: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2166
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 12H11" }),
2167
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 18H11" }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 6H11" }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m3 8 4 4-4 4" })
2170
+ ] }),
2171
+ quote: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2172
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M17 6H3" }),
2173
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 12H8" }),
2174
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 18H8" }),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 12v6" })
2176
+ ] }),
2177
+ pilcrow: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2178
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M13 4v16" }),
2179
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M17 4v16" }),
2180
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M19 4H9.5a4.5 4.5 0 0 0 0 9H13" })
2181
+ ] }),
2182
+ pageBreak: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2183
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "3", x2: "21", y1: "12", y2: "12" }),
2184
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "8 8 12 4 16 8" }),
2185
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "16 16 12 20 8 16" })
2186
+ ] }),
2187
+ divider: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2188
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 12h18" }),
2189
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 6h8", opacity: ".45" }),
2190
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 18h8", opacity: ".45" })
2191
+ ] }),
2192
+ layout: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2193
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "18", height: "7", x: "3", y: "3", rx: "1" }),
2194
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "9", height: "7", x: "3", y: "14", rx: "1" }),
2195
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "5", height: "7", x: "16", y: "14", rx: "1" })
2196
+ ] }),
2197
+ search: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2198
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "11", cy: "11", r: "8" }),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m21 21-4.3-4.3" })
2200
+ ] }),
2201
+ palette: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2202
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "13.5", cy: "6.5", r: ".5", fill: "currentColor" }),
2203
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "17.5", cy: "10.5", r: ".5", fill: "currentColor" }),
2204
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "8.5", cy: "7.5", r: ".5", fill: "currentColor" }),
2205
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "6.5", cy: "12.5", r: ".5", fill: "currentColor" }),
2206
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" })
2207
+ ] }),
2208
+ highlighter: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2209
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m9 11-6 6v3h9l3-3" }),
2210
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" })
2211
+ ] }),
2212
+ link: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2213
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
2215
+ ] }),
2216
+ image: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2217
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "9", cy: "9", r: "2" }),
2219
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21" })
2220
+ ] }),
2221
+ table: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2222
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 3v18" }),
2223
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 9h18" }),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 15h18" })
2226
+ ] }),
2227
+ eraser: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2228
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" }),
2229
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M22 21H7" }),
2230
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m5 11 9 9" })
2231
+ ] }),
2232
+ save: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2233
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" }),
2234
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" }),
2235
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M7 3v4a1 1 0 0 0 1 1h7" })
2236
+ ] }),
2237
+ download: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2238
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2239
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "7 10 12 15 17 10" }),
2240
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "12", x2: "12", y1: "15", y2: "3" })
2241
+ ] }),
2242
+ chevronDown: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m6 9 6 6 6-6" }),
2243
+ chevronUp: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m18 15-6-6-6 6" }),
2244
+ close: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M18 6 6 18" }),
2246
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m6 6 12 12" })
2247
+ ] }),
2248
+ maximize: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2249
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
2250
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }),
2251
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }),
2252
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" })
2253
+ ] }),
2254
+ copy: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2255
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2" }),
2256
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
2257
+ ] }),
2258
+ file: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2259
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
2260
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
2261
+ ] }),
2262
+ plus: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2263
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M5 12h14" }),
2264
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 5v14" })
2265
+ ] }),
2266
+ minus: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M5 12h14" }),
2267
+ check: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M20 6 9 17l-5-5" }),
2268
+ sparkles: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2269
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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)" }),
2270
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M20 3v4" }),
2271
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M22 5h-4" })
2272
+ ] }),
2273
+ upload: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2275
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "17 8 12 3 7 8" }),
2276
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "12", x2: "12", y1: "3", y2: "15" })
2277
+ ] })
2278
+ };
2279
+ function Icon({ name, size = 16 }) {
2280
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2281
+ "svg",
2282
+ {
2283
+ className: "june-doc__icon",
2284
+ width: size,
2285
+ height: size,
2286
+ viewBox: "0 0 24 24",
2287
+ fill: "none",
2288
+ stroke: "currentColor",
2289
+ strokeWidth: 2,
2290
+ strokeLinecap: "round",
2291
+ strokeLinejoin: "round",
2292
+ "aria-hidden": "true",
2293
+ focusable: "false",
2294
+ children: PATHS[name]
2295
+ }
2296
+ );
1365
2297
  }
1366
2298
 
1367
- // src/components/document-editor/document-editor.tsx
1368
- init_document_engine();
2299
+ // src/components/document-editor/document-popover.tsx
2300
+ var import_react7 = require("react");
2301
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2302
+ function Dropdown({ trigger, children, align = "start", className, onOpen }) {
2303
+ const [open, setOpen] = (0, import_react7.useState)(false);
2304
+ const anchor = (0, import_react7.useRef)(null);
2305
+ (0, import_react7.useEffect)(() => {
2306
+ if (!open) return;
2307
+ const onDown = (e) => {
2308
+ if (anchor.current && !anchor.current.contains(e.target)) setOpen(false);
2309
+ };
2310
+ document.addEventListener("mousedown", onDown);
2311
+ return () => document.removeEventListener("mousedown", onDown);
2312
+ }, [open]);
2313
+ const toggle = () => setOpen((v) => {
2314
+ if (!v) onOpen?.();
2315
+ return !v;
2316
+ });
2317
+ const close = () => setOpen(false);
2318
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2319
+ "div",
2320
+ {
2321
+ ref: anchor,
2322
+ className: "june-doc__anchor",
2323
+ onKeyDown: (e) => {
2324
+ if (e.key === "Escape" && open) {
2325
+ e.stopPropagation();
2326
+ close();
2327
+ }
2328
+ },
2329
+ children: [
2330
+ trigger({ open, toggle }),
2331
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: `june-doc__pop june-doc__pop--${align}${className ? ` ${className}` : ""}`, children: children(close) }) : null
2332
+ ]
2333
+ }
2334
+ );
2335
+ }
2336
+
2337
+ // src/components/document-editor/document-toolbar.tsx
2338
+ init_document_utils();
2339
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2340
+ var FONT_FAMILIES = [
2341
+ { label: "Default", value: "" },
2342
+ { label: "Arial", value: "Arial, Helvetica, sans-serif" },
2343
+ { label: "Calibri", value: "Calibri, Carlito, sans-serif" },
2344
+ { label: "Georgia", value: "Georgia, serif" },
2345
+ { label: "Garamond", value: "Garamond, 'EB Garamond', serif" },
2346
+ { label: "Times New Roman", value: "'Times New Roman', Times, serif" },
2347
+ { label: "Verdana", value: "Verdana, Geneva, sans-serif" },
2348
+ { label: "Trebuchet MS", value: "'Trebuchet MS', sans-serif" },
2349
+ { label: "Courier New", value: "'Courier New', Courier, monospace" }
2350
+ ];
2351
+ var FONT_SIZES = [8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 48, 60, 72];
2352
+ var TEXT_COLORS = [
2353
+ "#111827",
2354
+ "#374151",
2355
+ "#6b7280",
2356
+ "#9ca3af",
2357
+ "#dc2626",
2358
+ "#ea580c",
2359
+ "#ca8a04",
2360
+ "#16a34a",
2361
+ "#0d9488",
2362
+ "#0284c7",
2363
+ "#2563eb",
2364
+ "#4f46e5",
2365
+ "#9333ea",
2366
+ "#db2777",
2367
+ "#7f1d1d",
2368
+ "#1e3a8a"
2369
+ ];
2370
+ var HIGHLIGHT_COLORS = [
2371
+ "#fef08a",
2372
+ "#fde68a",
2373
+ "#fed7aa",
2374
+ "#fecaca",
2375
+ "#fbcfe8",
2376
+ "#e9d5ff",
2377
+ "#c7d2fe",
2378
+ "#bfdbfe",
2379
+ "#a5f3fc",
2380
+ "#bbf7d0",
2381
+ "#d9f99d",
2382
+ "#e5e7eb"
2383
+ ];
2384
+ var MAX_IMAGE_BYTES = 8 * 1024 * 1024;
2385
+ function Btn({ label, icon, active, disabled, toggle, onClick, children, className, text }) {
2386
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2387
+ "button",
2388
+ {
2389
+ type: "button",
2390
+ className: `june-doc__btn${icon && !text && !children ? " june-doc__btn--icon" : ""}${className ? ` ${className}` : ""}`,
2391
+ "aria-label": label,
2392
+ title: label,
2393
+ "aria-pressed": toggle ? !!active : void 0,
2394
+ "data-active": active || void 0,
2395
+ disabled,
2396
+ onMouseDown: (e) => e.preventDefault(),
2397
+ onClick,
2398
+ children: [
2399
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { name: icon }) : null,
2400
+ children ?? (text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: label }) : null)
2401
+ ]
2402
+ }
2403
+ );
2404
+ }
2405
+ function Group({ label, children }) {
2406
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", role: "group", "aria-label": label, children: [
2407
+ label ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "june-doc__group-label", "aria-hidden": "true", children: label }) : null,
2408
+ children
2409
+ ] });
2410
+ }
2411
+ function Field({ label, children }) {
2412
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "june-doc__field", children: [
2413
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "june-doc__field-label", children: label }),
2414
+ children
2415
+ ] });
2416
+ }
2417
+ function ColorPicker({ label, icon, colors, current, disabled, resetLabel, onPick, onReset }) {
2418
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2419
+ Dropdown,
2420
+ {
2421
+ trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2422
+ "button",
2423
+ {
2424
+ type: "button",
2425
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--color",
2426
+ "aria-label": label,
2427
+ title: label,
2428
+ "aria-expanded": open,
2429
+ "aria-haspopup": "dialog",
2430
+ disabled,
2431
+ onMouseDown: (e) => e.preventDefault(),
2432
+ onClick: toggle,
2433
+ children: [
2434
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { name: icon }),
2435
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "june-doc__color-bar", style: { background: current ?? "transparent" } })
2436
+ ]
2437
+ }
2438
+ ),
2439
+ children: (close) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__swatches-wrap", role: "dialog", "aria-label": label, children: [
2440
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__pop-title", children: label }),
2441
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__swatches", children: colors.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2442
+ "button",
2443
+ {
2444
+ type: "button",
2445
+ className: "june-doc__swatch",
2446
+ style: { background: c },
2447
+ "aria-label": c,
2448
+ title: c,
2449
+ "data-active": current?.toLowerCase() === c ? true : void 0,
2450
+ onMouseDown: (e) => e.preventDefault(),
2451
+ onClick: () => {
2452
+ onPick(c);
2453
+ close();
2454
+ }
2455
+ },
2456
+ c
2457
+ )) }),
2458
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__pop-row", children: [
2459
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2460
+ "button",
2461
+ {
2462
+ type: "button",
2463
+ className: "june-doc__btn june-doc__btn--ghost",
2464
+ onMouseDown: (e) => e.preventDefault(),
2465
+ onClick: () => {
2466
+ onReset();
2467
+ close();
2468
+ },
2469
+ children: resetLabel
2470
+ }
2471
+ ),
2472
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "june-doc__custom-color", title: "Custom color", children: [
2473
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Custom" }),
2474
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2475
+ "input",
2476
+ {
2477
+ type: "color",
2478
+ "aria-label": `${label}: custom`,
2479
+ defaultValue: current && /^#[0-9a-f]{6}$/i.test(current) ? current : "#000000",
2480
+ onChange: (e) => onPick(e.target.value)
2481
+ }
2482
+ )
2483
+ ] })
2484
+ ] })
2485
+ ] })
2486
+ }
2487
+ );
2488
+ }
2489
+ function LinkPanel({
2490
+ editor,
2491
+ href,
2492
+ isLink,
2493
+ close
2494
+ }) {
2495
+ const [value, setValue] = (0, import_react9.useState)(href);
2496
+ const apply = () => {
2497
+ const safe = sanitizeHref(value);
2498
+ if (safe) editor.chain().focus().extendMarkRange("link").setLink({ href: safe }).run();
2499
+ close();
2500
+ };
2501
+ const remove = () => {
2502
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
2503
+ close();
2504
+ };
2505
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__form", role: "dialog", "aria-label": "Edit link", children: [
2506
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__pop-title", children: "Link" }),
2507
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2508
+ "input",
2509
+ {
2510
+ "aria-label": "Link address",
2511
+ className: "june-doc__input",
2512
+ placeholder: "https://example.com",
2513
+ value,
2514
+ autoFocus: true,
2515
+ onChange: (e) => setValue(e.target.value),
2516
+ onKeyDown: (e) => {
2517
+ if (e.key === "Enter") {
2518
+ e.preventDefault();
2519
+ apply();
2520
+ }
2521
+ }
2522
+ }
2523
+ ),
2524
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__pop-row june-doc__pop-row--end", children: [
2525
+ isLink ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn june-doc__btn--ghost", onClick: remove, children: "Remove" }) : null,
2526
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn", onClick: close, children: "Cancel" }),
2527
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn june-doc__btn--primary", onClick: apply, children: "Apply" })
2528
+ ] })
2529
+ ] });
2530
+ }
2531
+ function ImagePanel({ editor, close, onNotice }) {
2532
+ const [url, setUrl] = (0, import_react9.useState)("");
2533
+ const [alt, setAlt] = (0, import_react9.useState)("");
2534
+ const file = (0, import_react9.useRef)(null);
2535
+ const insert = (src) => {
2536
+ editor.chain().focus().setImage({ src, alt: alt || void 0 }).run();
2537
+ close();
2538
+ };
2539
+ const fromUrl = () => {
2540
+ const safe = sanitizeHref(url);
2541
+ if (!safe || !/^https?:/i.test(safe)) {
2542
+ onNotice("Enter an http(s) image address");
2543
+ return;
2544
+ }
2545
+ insert(safe);
2546
+ };
2547
+ const fromFile = (f) => {
2548
+ if (!f) return;
2549
+ if (!f.type.startsWith("image/")) return onNotice("That file is not an image");
2550
+ if (f.size > MAX_IMAGE_BYTES) return onNotice("Images must be 8 MB or smaller");
2551
+ const reader = new FileReader();
2552
+ reader.onload = () => typeof reader.result === "string" && insert(reader.result);
2553
+ reader.onerror = () => onNotice("The image could not be read");
2554
+ reader.readAsDataURL(f);
2555
+ };
2556
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__form", role: "dialog", "aria-label": "Insert image", children: [
2557
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__pop-title", children: "Image" }),
2558
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2559
+ "button",
2560
+ {
2561
+ type: "button",
2562
+ className: "june-doc__dropzone",
2563
+ onClick: () => file.current?.click(),
2564
+ onDragOver: (e) => e.preventDefault(),
2565
+ onDrop: (e) => {
2566
+ e.preventDefault();
2567
+ fromFile(e.dataTransfer.files[0]);
2568
+ },
2569
+ children: [
2570
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { name: "upload", size: 18 }),
2571
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Upload or drop an image" })
2572
+ ]
2573
+ }
2574
+ ),
2575
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2576
+ "input",
2577
+ {
2578
+ ref: file,
2579
+ type: "file",
2580
+ accept: "image/*",
2581
+ hidden: true,
2582
+ onChange: (e) => fromFile(e.target.files?.[0])
2583
+ }
2584
+ ),
2585
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__or", children: "or" }),
2586
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2587
+ "input",
2588
+ {
2589
+ "aria-label": "Image address",
2590
+ className: "june-doc__input",
2591
+ placeholder: "https://example.com/image.png",
2592
+ value: url,
2593
+ onChange: (e) => setUrl(e.target.value),
2594
+ onKeyDown: (e) => e.key === "Enter" && fromUrl()
2595
+ }
2596
+ ),
2597
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2598
+ "input",
2599
+ {
2600
+ "aria-label": "Alternative text",
2601
+ className: "june-doc__input",
2602
+ placeholder: "Alt text (optional)",
2603
+ value: alt,
2604
+ onChange: (e) => setAlt(e.target.value)
2605
+ }
2606
+ ),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__pop-row june-doc__pop-row--end", children: [
2608
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn", onClick: close, children: "Cancel" }),
2609
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn june-doc__btn--primary", disabled: !url.trim(), onClick: fromUrl, children: "Insert" })
2610
+ ] })
2611
+ ] });
2612
+ }
2613
+ function TablePanel({ editor, close }) {
2614
+ const [hover, setHover] = (0, import_react9.useState)([0, 0]);
2615
+ const N = 8;
2616
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__form", role: "dialog", "aria-label": "Insert table", children: [
2617
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__pop-title", children: [
2618
+ "Table ",
2619
+ hover[0] ? `${hover[0]} \xD7 ${hover[1]}` : ""
2620
+ ] }),
2621
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__grid", onMouseLeave: () => setHover([0, 0]), children: Array.from({ length: N * N }, (_, i) => {
2622
+ const r = Math.floor(i / N) + 1;
2623
+ const c = i % N + 1;
2624
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2625
+ "button",
2626
+ {
2627
+ type: "button",
2628
+ className: "june-doc__cell",
2629
+ "aria-label": `${r} by ${c} table`,
2630
+ "data-active": r <= hover[0] && c <= hover[1] ? true : void 0,
2631
+ onMouseEnter: () => setHover([r, c]),
2632
+ onFocus: () => setHover([r, c]),
2633
+ onMouseDown: (e) => e.preventDefault(),
2634
+ onClick: () => {
2635
+ editor.chain().focus().insertTable({ rows: r, cols: c, withHeaderRow: true }).run();
2636
+ close();
2637
+ }
2638
+ },
2639
+ i
2640
+ );
2641
+ }) })
2642
+ ] });
2643
+ }
2644
+ var px = (v) => typeof v === "string" ? v.replace(/px$/, "") : "";
2645
+ function DocumentToolbar(props) {
2646
+ const { editor, editable, expanded, onToggleExpanded, showMarks, onToggleMarks, layout, onToggleLayout, findOpen, onToggleFind, onNotice } = props;
2647
+ const s = (0, import_react8.useEditorState)({
2648
+ editor,
2649
+ selector: ({ editor: ed }) => {
2650
+ if (!ed) return null;
2651
+ const ts = ed.getAttributes("textStyle");
2652
+ return {
2653
+ bold: ed.isActive("bold"),
2654
+ italic: ed.isActive("italic"),
2655
+ underline: ed.isActive("underline"),
2656
+ strike: ed.isActive("strike"),
2657
+ code: ed.isActive("code"),
2658
+ sup: ed.isActive("superscript"),
2659
+ sub: ed.isActive("subscript"),
2660
+ bullet: ed.isActive("bulletList"),
2661
+ ordered: ed.isActive("orderedList"),
2662
+ quote: ed.isActive("blockquote"),
2663
+ codeBlock: ed.isActive("codeBlock"),
2664
+ link: ed.isActive("link"),
2665
+ linkHref: ed.getAttributes("link").href ?? "",
2666
+ h1: ed.isActive("heading", { level: 1 }),
2667
+ h2: ed.isActive("heading", { level: 2 }),
2668
+ h3: ed.isActive("heading", { level: 3 }),
2669
+ left: ed.isActive({ textAlign: "left" }),
2670
+ center: ed.isActive({ textAlign: "center" }),
2671
+ right: ed.isActive({ textAlign: "right" }),
2672
+ justify: ed.isActive({ textAlign: "justify" }),
2673
+ inTable: ed.isActive("table"),
2674
+ color: ts.color ?? null,
2675
+ fontFamily: ts.fontFamily ?? "",
2676
+ fontSize: px(ts.fontSize),
2677
+ highlight: ed.isActive("highlight") ? ed.getAttributes("highlight").color ?? "#fef08a" : null,
2678
+ canUndo: ed.can().undo(),
2679
+ canRedo: ed.can().redo(),
2680
+ canSink: ed.can().sinkListItem("listItem"),
2681
+ canLift: ed.can().liftListItem("listItem"),
2682
+ canMerge: ed.can().mergeCells(),
2683
+ canSplit: ed.can().splitCell()
2684
+ };
2685
+ }
2686
+ });
2687
+ const run = () => editor?.chain().focus();
2688
+ const off = !editor || !editable;
2689
+ const style = s?.codeBlock ? "code" : s?.h1 ? "1" : s?.h2 ? "2" : s?.h3 ? "3" : s?.quote ? "quote" : "p";
2690
+ const setStyle = (v) => {
2691
+ if (!editor || !s) return;
2692
+ let c = editor.chain().focus();
2693
+ if (v === "quote") {
2694
+ if (!s.quote) c = c.setParagraph().toggleBlockquote();
2695
+ } else if (v === "code") {
2696
+ if (!s.codeBlock) c = c.toggleCodeBlock();
2697
+ } else {
2698
+ if (s.quote) c = c.lift("blockquote");
2699
+ c = v === "p" ? c.setParagraph() : c.setHeading({ level: Number(v) });
2700
+ }
2701
+ c.run();
2702
+ };
2703
+ const fontKnown = FONT_FAMILIES.some((f) => f.value === s?.fontFamily);
2704
+ const sizes = s?.fontSize && !FONT_SIZES.includes(Number(s.fontSize)) ? [...FONT_SIZES, Number(s.fontSize)].sort((a, b) => a - b) : FONT_SIZES;
2705
+ if (!editable) return null;
2706
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__ribbon", role: "toolbar", "aria-label": "Document formatting", children: [
2707
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__row", children: [
2708
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "History", children: [
2709
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Undo", icon: "undo", disabled: off || !s?.canUndo, onClick: () => run()?.undo().run() }),
2710
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Redo", icon: "redo", disabled: off || !s?.canRedo, onClick: () => run()?.redo().run() })
2711
+ ] }),
2712
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Format", children: [
2713
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Bold", icon: "bold", toggle: true, active: s?.bold, disabled: off, onClick: () => run()?.toggleBold().run() }),
2714
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Italic", icon: "italic", toggle: true, active: s?.italic, disabled: off, onClick: () => run()?.toggleItalic().run() }),
2715
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Underline", icon: "underline", toggle: true, active: s?.underline, disabled: off, onClick: () => run()?.toggleUnderline().run() }),
2716
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Strikethrough", icon: "strike", toggle: true, active: s?.strike, disabled: off, onClick: () => run()?.toggleStrike().run() }),
2717
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Inline code", icon: "code", toggle: true, active: s?.code, disabled: off, onClick: () => run()?.toggleCode().run() }),
2718
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Superscript", icon: "superscript", toggle: true, active: s?.sup, disabled: off, onClick: () => run()?.toggleSuperscript().run() }),
2719
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Subscript", icon: "subscript", toggle: true, active: s?.sub, disabled: off, onClick: () => run()?.toggleSubscript().run() })
2720
+ ] }),
2721
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Align", children: [
2722
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align left", icon: "alignLeft", toggle: true, active: s?.left, disabled: off, onClick: () => run()?.setTextAlign("left").run() }),
2723
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align center", icon: "alignCenter", toggle: true, active: s?.center, disabled: off, onClick: () => run()?.setTextAlign("center").run() }),
2724
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align right", icon: "alignRight", toggle: true, active: s?.right, disabled: off, onClick: () => run()?.setTextAlign("right").run() }),
2725
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Justify", icon: "alignJustify", toggle: true, active: s?.justify, disabled: off, onClick: () => run()?.setTextAlign("justify").run() })
2726
+ ] }),
2727
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Lists", children: [
2728
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Bullet list", icon: "list", toggle: true, active: s?.bullet, disabled: off, onClick: () => run()?.toggleBulletList().run() }),
2729
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Numbered list", icon: "listOrdered", toggle: true, active: s?.ordered, disabled: off, onClick: () => run()?.toggleOrderedList().run() }),
2730
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Decrease indent", icon: "outdent", disabled: off || !s?.canLift, onClick: () => run()?.liftListItem("listItem").run() }),
2731
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Increase indent", icon: "indent", disabled: off || !s?.canSink, onClick: () => run()?.sinkListItem("listItem").run() })
2732
+ ] }),
2733
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2734
+ "button",
2735
+ {
2736
+ type: "button",
2737
+ className: "june-doc__btn june-doc__more",
2738
+ "aria-expanded": expanded,
2739
+ "aria-label": expanded ? "Show fewer tools" : "Show more tools",
2740
+ onMouseDown: (e) => e.preventDefault(),
2741
+ onClick: onToggleExpanded,
2742
+ children: [
2743
+ expanded ? "Less" : "More",
2744
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { name: expanded ? "chevronUp" : "chevronDown", size: 14 })
2745
+ ]
2746
+ }
2747
+ )
2748
+ ] }),
2749
+ expanded ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__row june-doc__row--secondary", children: [
2750
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Style", children: [
2751
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Field, { label: "Style", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("select", { className: "june-doc__select", "aria-label": "Text style", disabled: off, value: style, onChange: (e) => setStyle(e.target.value), children: [
2752
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "p", children: "Paragraph" }),
2753
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "1", children: "Heading 1" }),
2754
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "2", children: "Heading 2" }),
2755
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "3", children: "Heading 3" }),
2756
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "quote", children: "Quote" }),
2757
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "code", children: "Code block" })
2758
+ ] }) }),
2759
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Field, { label: "Font", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2760
+ "select",
2761
+ {
2762
+ className: "june-doc__select june-doc__select--font",
2763
+ "aria-label": "Font family",
2764
+ disabled: off,
2765
+ value: s?.fontFamily ?? "",
2766
+ onChange: (e) => {
2767
+ const v = e.target.value;
2768
+ if (v) run()?.setFontFamily(v).run();
2769
+ else run()?.unsetFontFamily().run();
2770
+ },
2771
+ children: [
2772
+ FONT_FAMILIES.map((f) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: f.value, style: { fontFamily: f.value || void 0 }, children: f.label }, f.label)),
2773
+ !fontKnown && s?.fontFamily ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: s.fontFamily, children: s.fontFamily.split(",")[0].replace(/['"]/g, "") }) : null
2774
+ ]
2775
+ }
2776
+ ) }),
2777
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Field, { label: "Size", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2778
+ "select",
2779
+ {
2780
+ className: "june-doc__select june-doc__select--size",
2781
+ "aria-label": "Font size",
2782
+ disabled: off,
2783
+ value: s?.fontSize ?? "",
2784
+ onChange: (e) => {
2785
+ const v = e.target.value;
2786
+ if (v) run()?.setFontSize(`${v}px`).run();
2787
+ else run()?.unsetFontSize().run();
2788
+ },
2789
+ children: [
2790
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: "Auto" }),
2791
+ sizes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: String(n), children: n }, n))
2792
+ ]
2793
+ }
2794
+ ) })
2795
+ ] }),
2796
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Show formatting marks", icon: "pilcrow", toggle: true, active: showMarks, onClick: onToggleMarks, text: true, className: "june-doc__btn--marks", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "Paragraph" }) }),
2797
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Document", children: [
2798
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Insert page break", icon: "pageBreak", disabled: off, onClick: () => run()?.setPageBreak().run() }),
2799
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Insert divider", icon: "divider", disabled: off, onClick: () => run()?.setHorizontalRule().run() }),
2800
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: layout === "page" ? "Switch to full-width layout" : "Switch to page layout", icon: "layout", toggle: true, active: layout === "fluid", onClick: onToggleLayout }),
2801
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Find and replace", icon: "search", toggle: true, active: findOpen, onClick: onToggleFind })
2802
+ ] }),
2803
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Color", children: [
2804
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2805
+ ColorPicker,
2806
+ {
2807
+ label: "Text color",
2808
+ icon: "palette",
2809
+ colors: TEXT_COLORS,
2810
+ current: s?.color ?? null,
2811
+ disabled: off,
2812
+ resetLabel: "Default color",
2813
+ onPick: (c) => run()?.setColor(c).run(),
2814
+ onReset: () => run()?.unsetColor().run()
2815
+ }
2816
+ ),
2817
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2818
+ ColorPicker,
2819
+ {
2820
+ label: "Highlight",
2821
+ icon: "highlighter",
2822
+ colors: HIGHLIGHT_COLORS,
2823
+ current: s?.highlight ?? null,
2824
+ disabled: off,
2825
+ resetLabel: "No highlight",
2826
+ onPick: (c) => run()?.setHighlight({ color: c }).run(),
2827
+ onReset: () => run()?.unsetHighlight().run()
2828
+ }
2829
+ )
2830
+ ] }),
2831
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Insert", children: [
2832
+ editor ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2833
+ Dropdown,
2834
+ {
2835
+ trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Link", icon: "link", toggle: true, active: s?.link || open, disabled: off, onClick: toggle }),
2836
+ children: (close) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LinkPanel, { editor, href: s?.linkHref ?? "", isLink: !!s?.link, close })
2837
+ }
2838
+ ) : null,
2839
+ editor ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Insert image", icon: "image", active: open, disabled: off, onClick: toggle }), children: (close) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ImagePanel, { editor, close, onNotice }) }) : null,
2840
+ editor ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Insert table", icon: "table", active: open, disabled: off, onClick: toggle }), children: (close) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TablePanel, { editor, close }) }) : null
2841
+ ] }),
2842
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Group, { label: "Actions", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Clear formatting", icon: "eraser", disabled: off, onClick: () => run()?.unsetAllMarks().clearNodes().run() }) })
2843
+ ] }) : null,
2844
+ s?.inTable ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__row june-doc__row--context", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Group, { label: "Table", children: [
2845
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Add row below", disabled: off, onClick: () => run()?.addRowAfter().run(), children: "+ Row" }),
2846
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Add column right", disabled: off, onClick: () => run()?.addColumnAfter().run(), children: "+ Column" }),
2847
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete row", disabled: off, onClick: () => run()?.deleteRow().run(), children: "\u2212 Row" }),
2848
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete column", disabled: off, onClick: () => run()?.deleteColumn().run(), children: "\u2212 Column" }),
2849
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Merge cells", disabled: off || !s?.canMerge, onClick: () => run()?.mergeCells().run(), children: "Merge" }),
2850
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Split cell", disabled: off || !s?.canSplit, onClick: () => run()?.splitCell().run(), children: "Split" }),
2851
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Toggle header row", disabled: off, onClick: () => run()?.toggleHeaderRow().run(), children: "Header row" }),
2852
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete table", disabled: off, onClick: () => run()?.deleteTable().run(), className: "june-doc__btn--danger", children: "Delete table" })
2853
+ ] }) }) : null
2854
+ ] });
2855
+ }
2856
+
2857
+ // src/components/document-editor/document-bubble.tsx
2858
+ var import_jsx_runtime8 = require("react/jsx-runtime");
2859
+ function DocumentBubbleMenu({ editor }) {
2860
+ const s = (0, import_react10.useEditorState)({
2861
+ editor,
2862
+ selector: ({ editor: ed }) => ({
2863
+ bold: ed.isActive("bold"),
2864
+ italic: ed.isActive("italic"),
2865
+ underline: ed.isActive("underline"),
2866
+ strike: ed.isActive("strike"),
2867
+ link: ed.isActive("link"),
2868
+ linkHref: ed.getAttributes("link").href ?? "",
2869
+ color: ed.getAttributes("textStyle").color ?? null,
2870
+ highlight: ed.isActive("highlight") ? ed.getAttributes("highlight").color ?? "#fef08a" : null
2871
+ })
2872
+ });
2873
+ const run = () => editor.chain().focus();
2874
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2875
+ import_menus.BubbleMenu,
2876
+ {
2877
+ editor,
2878
+ className: "june-doc__bubble",
2879
+ options: { placement: "top", offset: 8 },
2880
+ shouldShow: ({ editor: ed, state }) => {
2881
+ const { empty } = state.selection;
2882
+ if (empty || !ed.isEditable) return false;
2883
+ if (ed.isActive("image") || ed.isActive("pageBreak") || ed.isActive("codeBlock")) return false;
2884
+ return state.doc.textBetween(state.selection.from, state.selection.to).trim().length > 0;
2885
+ },
2886
+ children: [
2887
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Bold", icon: "bold", toggle: true, active: s.bold, onClick: () => run().toggleBold().run() }),
2888
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Italic", icon: "italic", toggle: true, active: s.italic, onClick: () => run().toggleItalic().run() }),
2889
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Underline", icon: "underline", toggle: true, active: s.underline, onClick: () => run().toggleUnderline().run() }),
2890
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Strikethrough", icon: "strike", toggle: true, active: s.strike, onClick: () => run().toggleStrike().run() }),
2891
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "june-doc__bubble-sep", "aria-hidden": "true" }),
2892
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Dropdown, { trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Link", icon: "link", toggle: true, active: s.link || open, onClick: toggle }), children: (close) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LinkPanel, { editor, href: s.linkHref, isLink: s.link, close }) }),
2893
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2894
+ ColorPicker,
2895
+ {
2896
+ label: "Text color",
2897
+ icon: "palette",
2898
+ colors: TEXT_COLORS,
2899
+ current: s.color,
2900
+ resetLabel: "Default color",
2901
+ onPick: (c) => run().setColor(c).run(),
2902
+ onReset: () => run().unsetColor().run()
2903
+ }
2904
+ ),
2905
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2906
+ ColorPicker,
2907
+ {
2908
+ label: "Highlight",
2909
+ icon: "highlighter",
2910
+ colors: HIGHLIGHT_COLORS,
2911
+ current: s.highlight,
2912
+ resetLabel: "No highlight",
2913
+ onPick: (c) => run().setHighlight({ color: c }).run(),
2914
+ onReset: () => run().unsetHighlight().run()
2915
+ }
2916
+ ),
2917
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "june-doc__bubble-sep", "aria-hidden": "true" }),
2918
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Btn, { label: "Clear formatting", icon: "eraser", onClick: () => run().unsetAllMarks().run() })
2919
+ ]
2920
+ }
2921
+ );
2922
+ }
2923
+
2924
+ // src/components/document-editor/document-content.tsx
2925
+ var import_react11 = require("@tiptap/react");
2926
+ var import_react12 = require("react");
2927
+ init_document_pagination();
2928
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2929
+ var PAGE_WIDTH = 794;
2930
+ var PAGE_HEIGHT = 1123;
2931
+ var PAGE_GAP = 28;
2932
+ var CANVAS_PAD_X = 24;
2933
+ var useIsoLayoutEffect = typeof window === "undefined" ? import_react12.useEffect : import_react12.useLayoutEffect;
2934
+ function geometry(width) {
2935
+ const ratio = width / PAGE_WIDTH;
2936
+ const height = Math.round(PAGE_HEIGHT * ratio);
2937
+ const padX = Math.round(Math.min(96, Math.max(20, width * 0.12)));
2938
+ const padY = Math.round(Math.min(96, Math.max(36, 96 * ratio)));
2939
+ return { width, height, padX, padY };
2940
+ }
2941
+ function DocumentContent({ editor, layout = "page", zoom = 1, showMarks = false, onMetrics }) {
2942
+ const canvas = (0, import_react12.useRef)(null);
2943
+ const sheet = (0, import_react12.useRef)(null);
2944
+ const [avail, setAvail] = (0, import_react12.useState)(PAGE_WIDTH);
2945
+ const [sheetH, setSheetH] = (0, import_react12.useState)(0);
2946
+ const [pages, setPages] = (0, import_react12.useState)(1);
2947
+ const metricsRef = (0, import_react12.useRef)(onMetrics);
2948
+ metricsRef.current = onMetrics;
2949
+ const last = (0, import_react12.useRef)({ page: 0, pages: 0 });
2950
+ const paged = layout === "page";
2951
+ const width = paged ? Math.max(260, Math.min(PAGE_WIDTH, Math.floor(avail / zoom))) : Math.max(260, Math.floor(avail / zoom));
2952
+ const g = geometry(width);
2953
+ const stride = g.height + PAGE_GAP;
2954
+ useIsoLayoutEffect(() => {
2955
+ const c = canvas.current;
2956
+ if (!c) return;
2957
+ const read = () => {
2958
+ const cs = typeof getComputedStyle === "function" ? getComputedStyle(c) : null;
2959
+ const padX = cs ? (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) : CANVAS_PAD_X * 2;
2960
+ const w = c.clientWidth - padX;
2961
+ if (w > 0) setAvail(w);
2962
+ };
2963
+ read();
2964
+ if (typeof ResizeObserver === "undefined") return;
2965
+ const ro = new ResizeObserver(read);
2966
+ ro.observe(c);
2967
+ return () => ro.disconnect();
2968
+ }, []);
2969
+ (0, import_react12.useEffect)(() => {
2970
+ if (!editor) return;
2971
+ setPaginationConfig(editor, {
2972
+ enabled: paged,
2973
+ pageHeight: g.height,
2974
+ gap: PAGE_GAP,
2975
+ padTop: g.padY,
2976
+ padBottom: g.padY
2977
+ });
2978
+ }, [editor, paged, g.height, g.padY, width]);
2979
+ (0, import_react12.useEffect)(() => {
2980
+ const c = canvas.current;
2981
+ const s = sheet.current;
2982
+ if (!editor || !c || !s) return;
2983
+ const prose = editor.view.dom;
2984
+ const report = () => {
2985
+ const total = paged ? getPageCount(editor) : 1;
2986
+ setPages(total);
2987
+ setSheetH(s.offsetHeight);
2988
+ const cRect = c.getBoundingClientRect();
2989
+ const sRect = s.getBoundingClientRect();
2990
+ const y = (cRect.top + c.clientHeight * 0.35 - sRect.top) / zoom;
2991
+ const page = paged ? Math.min(total, Math.max(1, Math.floor(y / stride) + 1)) : 1;
2992
+ if (last.current.page !== page || last.current.pages !== total) {
2993
+ last.current = { page, pages: total };
2994
+ metricsRef.current?.({ page, pages: total });
2995
+ }
2996
+ };
2997
+ report();
2998
+ editor.on("transaction", report);
2999
+ c.addEventListener("scroll", report, { passive: true });
3000
+ const onLoad = () => remeasurePagination(editor);
3001
+ prose.addEventListener("load", onLoad, true);
3002
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(report) : null;
3003
+ ro?.observe(s);
3004
+ return () => {
3005
+ editor.off("transaction", report);
3006
+ c.removeEventListener("scroll", report);
3007
+ prose.removeEventListener("load", onLoad, true);
3008
+ ro?.disconnect();
3009
+ };
3010
+ }, [editor, paged, zoom, stride]);
3011
+ const minHeight = paged ? pages * g.height + (pages - 1) * PAGE_GAP : void 0;
3012
+ const sheetStyle = {
3013
+ width,
3014
+ minHeight,
3015
+ transform: zoom === 1 ? void 0 : `scale(${zoom})`,
3016
+ "--jd-pad-x": `${g.padX}px`,
3017
+ "--jd-pad-y": `${g.padY}px`
3018
+ };
3019
+ const zoomerStyle = {
3020
+ width: width * zoom,
3021
+ height: zoom === 1 ? void 0 : Math.max(sheetH, minHeight ?? 0) * zoom
3022
+ };
3023
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: canvas, className: "june-doc__content june-doc__canvas", "data-layout": layout, "data-marks": showMarks || void 0, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__zoomer", style: zoomerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { ref: sheet, className: "june-doc__sheet", style: sheetStyle, children: [
3024
+ paged ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__pages", "aria-hidden": "true", children: Array.from({ length: pages }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "june-doc__page", style: { top: i * stride, height: g.height }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "june-doc__page-num", children: [
3025
+ i + 1,
3026
+ " / ",
3027
+ pages
3028
+ ] }) }, i)) }) : null,
3029
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react11.EditorContent, { editor })
3030
+ ] }) }) });
3031
+ }
3032
+
3033
+ // src/components/document-editor/document-findbar.tsx
3034
+ var import_react13 = require("@tiptap/react");
3035
+ var import_react14 = require("react");
3036
+ init_document_search();
3037
+ var import_jsx_runtime10 = require("react/jsx-runtime");
3038
+ function DocumentFindBar({ editor, editable, onClose, onNotice }) {
3039
+ const [term, setTerm] = (0, import_react14.useState)(() => {
3040
+ const { from, to, empty } = editor.state.selection;
3041
+ return empty ? "" : editor.state.doc.textBetween(from, to, " ").slice(0, 100);
3042
+ });
3043
+ const [replacement, setReplacement] = (0, import_react14.useState)("");
3044
+ const [caseSensitive, setCaseSensitive] = (0, import_react14.useState)(false);
3045
+ const input = (0, import_react14.useRef)(null);
3046
+ const info = (0, import_react13.useEditorState)({
3047
+ editor,
3048
+ selector: ({ editor: ed }) => {
3049
+ const s = getSearchState(ed);
3050
+ return { count: s?.matches.length ?? 0, current: s?.current ?? -1 };
3051
+ }
3052
+ });
3053
+ (0, import_react14.useEffect)(() => {
3054
+ input.current?.focus();
3055
+ input.current?.select();
3056
+ }, []);
3057
+ (0, import_react14.useEffect)(() => {
3058
+ setSearch(editor, { term, caseSensitive, current: 0 });
3059
+ }, [editor, term, caseSensitive]);
3060
+ (0, import_react14.useEffect)(() => () => {
3061
+ if (!editor.isDestroyed) setSearch(editor, { term: "" });
3062
+ }, [editor]);
3063
+ const count = info?.count ?? 0;
3064
+ const current = info?.current ?? -1;
3065
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
3066
+ "div",
3067
+ {
3068
+ className: "june-doc__findbar",
3069
+ role: "search",
3070
+ onKeyDown: (e) => {
3071
+ if (e.key === "Escape") {
3072
+ e.stopPropagation();
3073
+ onClose();
3074
+ editor.commands.focus();
3075
+ }
3076
+ },
3077
+ children: [
3078
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "june-doc__find-field", children: [
3079
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "search", size: 14 }),
3080
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3081
+ "input",
3082
+ {
3083
+ ref: input,
3084
+ className: "june-doc__find-input",
3085
+ "aria-label": "Find in document",
3086
+ placeholder: "Find in document",
3087
+ value: term,
3088
+ onChange: (e) => setTerm(e.target.value),
3089
+ onKeyDown: (e) => {
3090
+ if (e.key === "Enter") {
3091
+ e.preventDefault();
3092
+ gotoMatch(editor, e.shiftKey ? -1 : 1);
3093
+ }
3094
+ }
3095
+ }
3096
+ ),
3097
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "june-doc__find-count", "aria-live": "polite", children: term ? count ? `${current + 1} of ${count}` : "No results" : "" })
3098
+ ] }),
3099
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("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__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "chevronUp" }) }),
3100
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("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__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "chevronDown" }) }),
3101
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3102
+ "button",
3103
+ {
3104
+ type: "button",
3105
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--case",
3106
+ "aria-label": "Match case",
3107
+ title: "Match case",
3108
+ "aria-pressed": caseSensitive,
3109
+ "data-active": caseSensitive || void 0,
3110
+ onClick: () => setCaseSensitive((v) => !v),
3111
+ children: "Aa"
3112
+ }
3113
+ ),
3114
+ editable ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3115
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "june-doc__find-sep", "aria-hidden": "true" }),
3116
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3117
+ "input",
3118
+ {
3119
+ className: "june-doc__input june-doc__replace-input",
3120
+ "aria-label": "Replace with",
3121
+ placeholder: "Replace with",
3122
+ value: replacement,
3123
+ onChange: (e) => setReplacement(e.target.value),
3124
+ onKeyDown: (e) => {
3125
+ if (e.key === "Enter") {
3126
+ e.preventDefault();
3127
+ replaceCurrent(editor, replacement);
3128
+ }
3129
+ }
3130
+ }
3131
+ ),
3132
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "june-doc__btn", disabled: !count, onClick: () => replaceCurrent(editor, replacement), children: "Replace" }),
3133
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3134
+ "button",
3135
+ {
3136
+ type: "button",
3137
+ className: "june-doc__btn",
3138
+ disabled: !count,
3139
+ onClick: () => {
3140
+ const n = replaceAll(editor, replacement);
3141
+ onNotice(`Replaced ${n} ${n === 1 ? "match" : "matches"}`);
3142
+ },
3143
+ children: "Replace all"
3144
+ }
3145
+ )
3146
+ ] }) : null,
3147
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("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__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "close" }) })
3148
+ ]
3149
+ }
3150
+ );
3151
+ }
3152
+
3153
+ // src/components/document-editor/document-title.tsx
3154
+ var import_jsx_runtime11 = require("react/jsx-runtime");
3155
+ function DocumentTitle({ value, disabled, onChange }) {
3156
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3157
+ "input",
3158
+ {
3159
+ className: "june-doc__title",
3160
+ "aria-label": "Document title",
3161
+ value,
3162
+ disabled,
3163
+ placeholder: "Untitled document",
3164
+ onChange: (e) => onChange(e.target.value)
3165
+ }
3166
+ );
3167
+ }
3168
+
3169
+ // src/components/document-editor/document-header.tsx
3170
+ var import_jsx_runtime12 = require("react/jsx-runtime");
3171
+ var FORMATS = {
3172
+ docx: { label: "Word document", hint: ".docx", icon: "file" },
3173
+ pdf: { label: "PDF", hint: "Print / Save as PDF", icon: "file" },
3174
+ html: { label: "Web page", hint: ".html", icon: "code" },
3175
+ markdown: { label: "Markdown", hint: ".md", icon: "file" },
3176
+ txt: { label: "Plain text", hint: ".txt", icon: "file" }
3177
+ };
3178
+ function DocumentHeader(props) {
3179
+ const { title, editable, isDraft, fullscreen, onToggleFullscreen, onTitle, exportFormats, canSave, showSave = canSave, saveState, onExport, onCopy, onSave } = props;
3180
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "june-doc__header", children: [
3181
+ fullscreen ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("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__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "close", size: 18 }) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "june-doc__doc-icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "file", size: 16 }) }),
3182
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DocumentTitle, { value: title, disabled: !editable, onChange: onTitle }),
3183
+ isDraft ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "june-doc__badge", children: [
3184
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "sparkles", size: 12 }),
3185
+ "AI draft"
3186
+ ] }) : null,
3187
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "june-doc__header-actions", children: [
3188
+ showSave ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3189
+ "button",
3190
+ {
3191
+ type: "button",
3192
+ className: "june-doc__btn june-doc__btn--outline june-doc__save",
3193
+ "data-state": saveState,
3194
+ "aria-label": "Save",
3195
+ title: "Save (Ctrl+S)",
3196
+ disabled: saveState === "saving" || saveState === "saved" || saveState === "idle",
3197
+ onClick: onSave,
3198
+ children: [
3199
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: saveState === "saved" ? "check" : "save" }),
3200
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: saveState === "saving" ? "Saving\u2026" : saveState === "saved" ? "Saved" : "Save" })
3201
+ ]
3202
+ }
3203
+ ) : null,
3204
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3205
+ Dropdown,
3206
+ {
3207
+ align: "end",
3208
+ className: "june-doc__menu",
3209
+ trigger: ({ open, toggle }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3210
+ "button",
3211
+ {
3212
+ type: "button",
3213
+ className: "june-doc__btn june-doc__btn--solid",
3214
+ "aria-label": "Download",
3215
+ "aria-haspopup": "menu",
3216
+ "aria-expanded": open,
3217
+ onClick: toggle,
3218
+ children: [
3219
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: "Download" }),
3220
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "chevronDown", size: 14 })
3221
+ ]
3222
+ }
3223
+ ),
3224
+ children: (close) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { role: "menu", "aria-label": "Download options", children: [
3225
+ exportFormats.map((f) => {
3226
+ const meta = FORMATS[f] ?? { label: f.toUpperCase(), hint: "", icon: "file" };
3227
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3228
+ "button",
3229
+ {
3230
+ type: "button",
3231
+ role: "menuitem",
3232
+ className: "june-doc__menu-item",
3233
+ onClick: () => {
3234
+ close();
3235
+ onExport(f);
3236
+ },
3237
+ children: [
3238
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: meta.icon }),
3239
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "june-doc__menu-label", children: meta.label }),
3240
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "june-doc__menu-hint", children: meta.hint })
3241
+ ]
3242
+ },
3243
+ f
3244
+ );
3245
+ }),
3246
+ exportFormats.length ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "june-doc__menu-sep", role: "separator" }) : null,
3247
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
3248
+ "button",
3249
+ {
3250
+ type: "button",
3251
+ role: "menuitem",
3252
+ className: "june-doc__menu-item",
3253
+ "aria-label": "Copy document",
3254
+ onClick: () => {
3255
+ close();
3256
+ onCopy();
3257
+ },
3258
+ children: [
3259
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "copy" }),
3260
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "june-doc__menu-label", children: "Copy to clipboard" })
3261
+ ]
3262
+ }
3263
+ )
3264
+ ] })
3265
+ }
3266
+ ),
3267
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3268
+ "button",
3269
+ {
3270
+ type: "button",
3271
+ className: "june-doc__btn june-doc__btn--icon june-doc__btn--ghost",
3272
+ "aria-label": fullscreen ? "Exit full screen" : "Open full screen",
3273
+ title: fullscreen ? "Exit full screen" : "Open full screen",
3274
+ "aria-pressed": fullscreen,
3275
+ onClick: onToggleFullscreen,
3276
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { name: "maximize" })
3277
+ }
3278
+ )
3279
+ ] })
3280
+ ] });
3281
+ }
3282
+
3283
+ // src/components/document-editor/document-editor.tsx
3284
+ init_document_engine();
1369
3285
 
1370
3286
  // src/components/document-editor/document-export.ts
1371
3287
  init_errors();
1372
3288
  init_document_utils();
1373
3289
  var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1374
- 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}}';
3290
+ 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}}';
1375
3291
  function buildHtmlDocument(title, bodyHtml) {
1376
3292
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(
1377
3293
  title
@@ -1409,6 +3325,33 @@ var exportTxt = async (ctx) => {
1409
3325
  ${docToText2(ctx.json)}
1410
3326
  `, `${ctx.basename}.txt`, "text/plain");
1411
3327
  };
3328
+ function toDocxColor(value) {
3329
+ if (typeof value !== "string") return void 0;
3330
+ const v = value.trim();
3331
+ let m = /^#([0-9a-f]{3})$/i.exec(v);
3332
+ if (m) return m[1].split("").map((c) => c + c).join("").toUpperCase();
3333
+ m = /^#([0-9a-f]{6})$/i.exec(v);
3334
+ if (m) return m[1].toUpperCase();
3335
+ m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(v);
3336
+ if (m) {
3337
+ return [m[1], m[2], m[3]].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("").toUpperCase();
3338
+ }
3339
+ return void 0;
3340
+ }
3341
+ function toHalfPoints(value) {
3342
+ if (typeof value !== "string") return void 0;
3343
+ const m = /^([\d.]+)\s*(px|pt)?$/i.exec(value.trim());
3344
+ if (!m) return void 0;
3345
+ const n = Number(m[1]);
3346
+ if (!Number.isFinite(n) || n <= 0) return void 0;
3347
+ const pt = m[2]?.toLowerCase() === "pt" ? n : n * 0.75;
3348
+ return Math.round(pt * 2);
3349
+ }
3350
+ function firstFontFamily(value) {
3351
+ if (typeof value !== "string") return void 0;
3352
+ const first = value.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
3353
+ return first || void 0;
3354
+ }
1412
3355
  var ORDERED_REF = "june-ordered";
1413
3356
  var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
1414
3357
  function runs(d, node, fmt) {
@@ -1422,14 +3365,21 @@ function runs(d, node, fmt) {
1422
3365
  const marks = child.marks ?? [];
1423
3366
  const has = (type) => marks.some((m) => m.type === type);
1424
3367
  const link = marks.find((m) => m.type === "link");
3368
+ const style = marks.find((m) => m.type === "textStyle")?.attrs ?? {};
3369
+ const highlight = marks.find((m) => m.type === "highlight");
3370
+ const fill = highlight ? toDocxColor(highlight.attrs?.color) ?? "FFF59D" : void 0;
1425
3371
  const run = new d.TextRun({
1426
3372
  text: child.text,
1427
3373
  bold: fmt.bold || has("bold") || void 0,
1428
3374
  italics: has("italic") || void 0,
1429
3375
  underline: has("underline") || link ? {} : void 0,
1430
3376
  strike: has("strike") || void 0,
1431
- font: has("code") ? "Courier New" : void 0,
1432
- color: link ? "0563C1" : void 0
3377
+ superScript: has("superscript") || void 0,
3378
+ subScript: has("subscript") || void 0,
3379
+ font: has("code") ? "Courier New" : firstFontFamily(style.fontFamily),
3380
+ size: toHalfPoints(style.fontSize),
3381
+ color: toDocxColor(style.color) ?? (link ? "0563C1" : void 0),
3382
+ shading: fill ? { type: d.ShadingType.CLEAR, fill, color: "auto" } : void 0
1433
3383
  });
1434
3384
  const href = typeof link?.attrs?.href === "string" ? sanitizeHref(link.attrs.href) : null;
1435
3385
  out.push(href ? new d.ExternalHyperlink({ link: href, children: [run] }) : run);
@@ -1498,563 +3448,262 @@ function table(d, node, state) {
1498
3448
  if (rows.length === 0) return new d.Paragraph("");
1499
3449
  return new d.Table({ rows, width: { size: 100, type: d.WidthType.PERCENTAGE } });
1500
3450
  }
1501
- function blocks(d, nodes, fmt, state) {
1502
- const headings = [d.HeadingLevel.HEADING_1, d.HeadingLevel.HEADING_2, d.HeadingLevel.HEADING_3];
1503
- const out = [];
1504
- for (const node of nodes ?? []) {
1505
- switch (node.type) {
1506
- case "paragraph":
1507
- out.push(paragraph(d, node, fmt));
1508
- break;
1509
- case "heading": {
1510
- const level = Math.min(Math.max(Number(node.attrs?.level) || 1, 1), 3);
1511
- out.push(paragraph(d, node, fmt, { heading: headings[level - 1] }));
1512
- break;
1513
- }
1514
- case "bulletList":
1515
- case "orderedList":
1516
- out.push(...list(d, node, fmt, 0, state));
1517
- break;
1518
- case "blockquote":
1519
- out.push(...blocks(d, node.content, { ...fmt, quote: true, indent: (fmt.indent ?? 0) + 360 }, state));
1520
- break;
1521
- case "codeBlock":
1522
- out.push(code(d, node));
1523
- break;
1524
- case "horizontalRule":
1525
- out.push(
1526
- new d.Paragraph({
1527
- border: { bottom: { color: "9CA3AF", space: 1, style: d.BorderStyle.SINGLE, size: 6 } },
1528
- spacing: { after: 120 }
3451
+ function image(d, node, state) {
3452
+ const src = typeof node.attrs?.src === "string" ? node.attrs.src : "";
3453
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
3454
+ const img = state.images.get(src);
3455
+ if (img) {
3456
+ return [
3457
+ new d.Paragraph({
3458
+ children: [
3459
+ new d.ImageRun({
3460
+ type: img.type,
3461
+ data: img.data,
3462
+ transformation: { width: img.width, height: img.height },
3463
+ altText: alt ? { name: alt, description: alt, title: alt } : void 0
1529
3464
  })
1530
- );
1531
- break;
1532
- case "table":
1533
- out.push(table(d, node, state));
1534
- break;
1535
- default:
1536
- if (node.content) out.push(...blocks(d, node.content, fmt, state));
1537
- }
1538
- }
1539
- return out;
1540
- }
1541
- var exportDocx = async (ctx) => {
1542
- const d = await import("docx");
1543
- const state = { instance: 0 };
1544
- const children = [
1545
- new d.Paragraph({ text: ctx.title, heading: d.HeadingLevel.TITLE }),
1546
- ...blocks(d, ctx.json.content, {}, state)
1547
- ];
1548
- const doc = new d.Document({
1549
- title: ctx.title,
1550
- numbering: {
1551
- config: [
1552
- {
1553
- reference: ORDERED_REF,
1554
- levels: [0, 1, 2].map((level) => ({
1555
- level,
1556
- format: d.LevelFormat.DECIMAL,
1557
- text: `%${level + 1}.`,
1558
- alignment: d.AlignmentType.START,
1559
- style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
1560
- }))
1561
- }
1562
- ]
1563
- },
1564
- sections: [{ children }]
1565
- });
1566
- const blob = await d.Packer.toBlob(doc);
1567
- return { filename: `${ctx.basename}.docx`, mimeType: DOCX_MIME, blob };
1568
- };
1569
- function printHtml(html) {
1570
- return new Promise((resolve, reject) => {
1571
- const frame = document.createElement("iframe");
1572
- frame.setAttribute("aria-hidden", "true");
1573
- frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
1574
- const cleanup = () => setTimeout(() => frame.remove(), 500);
1575
- frame.onload = () => {
1576
- try {
1577
- const win = frame.contentWindow;
1578
- if (!win) throw new Error("The print frame is unavailable");
1579
- win.addEventListener("afterprint", cleanup);
1580
- win.focus();
1581
- win.print();
1582
- setTimeout(cleanup, 12e4);
1583
- resolve();
1584
- } catch (error) {
1585
- cleanup();
1586
- reject(error);
1587
- }
1588
- };
1589
- frame.srcdoc = html;
1590
- document.body.appendChild(frame);
1591
- });
1592
- }
1593
- var exportPdf = async (ctx) => {
1594
- const { docToHtml: docToHtml2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
1595
- await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
1596
- return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
1597
- };
1598
- var registry = /* @__PURE__ */ new Map([
1599
- ["html", exportHtml],
1600
- ["markdown", exportMarkdown],
1601
- ["txt", exportTxt],
1602
- ["docx", exportDocx],
1603
- ["pdf", exportPdf]
1604
- ]);
1605
- function registerDocumentExporter(format, exporter) {
1606
- registry.set(format, exporter);
1607
- }
1608
- function listDocumentExporters() {
1609
- return [...registry.keys()];
1610
- }
1611
- async function exportDocument(format, doc) {
1612
- const exporter = registry.get(format);
1613
- if (!exporter) throw new JuneError("EXPORT_ERROR", `No exporter is registered for "${format}"`);
1614
- try {
1615
- return await exporter({ ...doc, basename: slugify(doc.title) || "document" });
1616
- } catch (error) {
1617
- throw toJuneError(error, "EXPORT_ERROR", `Exporting ${format} failed`);
3465
+ ],
3466
+ spacing: { after: 120 }
3467
+ })
3468
+ ];
1618
3469
  }
1619
- }
1620
- function downloadResult(result) {
1621
- if (!result.blob) return;
1622
- const url = URL.createObjectURL(result.blob);
1623
- const link = document.createElement("a");
1624
- link.href = url;
1625
- link.download = result.filename;
1626
- document.body.appendChild(link);
1627
- link.click();
1628
- link.remove();
1629
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
1630
- }
1631
- async function copyDocumentToClipboard(doc) {
1632
- const { docToHtml: docToHtml2, docToText: docToText2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
1633
- const html = docToHtml2(doc.json);
1634
- const text = `${doc.title}
1635
-
1636
- ${docToText2(doc.json)}`;
1637
- try {
1638
- if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
1639
- await navigator.clipboard.write([
1640
- new ClipboardItem({
1641
- "text/html": new Blob([html], { type: "text/html" }),
1642
- "text/plain": new Blob([text], { type: "text/plain" })
3470
+ const href = src && !src.startsWith("data:") ? sanitizeHref(src) : null;
3471
+ if (!href) return [];
3472
+ return [
3473
+ new d.Paragraph({
3474
+ children: [
3475
+ new d.ExternalHyperlink({
3476
+ link: href,
3477
+ children: [new d.TextRun({ text: alt || href, color: "0563C1", underline: {} })]
1643
3478
  })
1644
- ]);
1645
- return true;
1646
- }
1647
- await navigator.clipboard.writeText(text);
1648
- return true;
1649
- } catch {
1650
- return false;
1651
- }
1652
- }
1653
-
1654
- // src/components/document-editor/document-editor.tsx
1655
- init_document_extensions();
1656
-
1657
- // src/components/document-editor/document-merge.ts
1658
- init_document_utils();
1659
- var MAX_CELLS = 4e6;
1660
- function matchIndices(a, b) {
1661
- const n = a.length;
1662
- const m = b.length;
1663
- if (n * m > MAX_CELLS) return null;
1664
- const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
1665
- for (let i2 = n - 1; i2 >= 0; i2--) {
1666
- for (let j2 = m - 1; j2 >= 0; j2--) {
1667
- dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
1668
- }
1669
- }
1670
- const matches = /* @__PURE__ */ new Map();
1671
- let i = 0;
1672
- let j = 0;
1673
- while (i < n && j < m) {
1674
- if (a[i] === b[j]) {
1675
- matches.set(i, j);
1676
- i++;
1677
- j++;
1678
- } else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
1679
- else j++;
1680
- }
1681
- return matches;
3479
+ ]
3480
+ })
3481
+ ];
1682
3482
  }
1683
- var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
1684
- function mergeBlocks(base, local, incoming) {
1685
- if (same(local, base)) return { blocks: incoming, conflicts: 0 };
1686
- if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
1687
- const ml = matchIndices(base, local);
1688
- const mn = matchIndices(base, incoming);
1689
- if (!ml || !mn) return { blocks: null, conflicts: 1 };
1690
- const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
1691
- const out = [];
1692
- let conflicts = 0;
1693
- let pb = -1;
1694
- let pl = -1;
1695
- let pn = -1;
1696
- const region = (bEnd, lEnd, nEnd) => {
1697
- const B = base.slice(pb + 1, bEnd);
1698
- const L = local.slice(pl + 1, lEnd);
1699
- const N = incoming.slice(pn + 1, nEnd);
1700
- if (B.length === L.length && L.length === N.length) {
1701
- for (let k = 0; k < B.length; k++) {
1702
- if (L[k] === B[k]) out.push(N[k]);
1703
- else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
1704
- else {
1705
- conflicts++;
1706
- out.push(L[k]);
1707
- }
1708
- }
1709
- return;
1710
- }
1711
- if (same(L, B)) out.push(...N);
1712
- else if (same(N, B) || same(L, N)) out.push(...L);
1713
- else {
1714
- conflicts++;
1715
- out.push(...L);
1716
- }
1717
- };
1718
- for (const a of anchors) {
1719
- const la = ml.get(a);
1720
- const na = mn.get(a);
1721
- region(a, la, na);
1722
- out.push(local[la]);
1723
- pb = a;
1724
- pl = la;
1725
- pn = na;
3483
+ function collectImages(nodes, out) {
3484
+ for (const node of nodes ?? []) {
3485
+ const src = node.attrs?.src;
3486
+ if (node.type === "image" && typeof src === "string" && src && !out.has(src)) out.set(src, node.attrs);
3487
+ if (node.content) collectImages(node.content, out);
1726
3488
  }
1727
- region(base.length, local.length, incoming.length);
1728
- return { blocks: out, conflicts };
1729
- }
1730
- function mergeDocuments(base, local, incoming) {
1731
- const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
1732
- const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
1733
- if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
1734
- if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
1735
- return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
3489
+ return out;
1736
3490
  }
1737
-
1738
- // src/components/document-editor/document-store.ts
1739
- init_document_utils();
1740
- var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
1741
- function createDocumentStore() {
1742
- const records = /* @__PURE__ */ new Map();
1743
- const listeners = /* @__PURE__ */ new Set();
1744
- const emit = () => listeners.forEach((listener) => listener());
1745
- const put = (record) => {
1746
- records.set(record.id, record);
1747
- emit();
1748
- };
1749
- const fresh2 = (doc) => ({
1750
- id: doc.id,
1751
- title: doc.title,
1752
- documentType: doc.documentType,
1753
- status: doc.status,
1754
- json: doc.json,
1755
- baseJson: doc.json,
1756
- baseTitle: doc.title,
1757
- lastIncomingKey: incomingKey(doc),
1758
- revision: 0,
1759
- origin: "external",
1760
- pending: null,
1761
- updatedAt: Date.now()
3491
+ var IMAGE_TYPES = {
3492
+ "image/png": "png",
3493
+ "image/jpeg": "jpg",
3494
+ "image/jpg": "jpg",
3495
+ "image/gif": "gif",
3496
+ "image/bmp": "bmp"
3497
+ };
3498
+ var MAX_IMAGE_WIDTH = 600;
3499
+ function naturalSize(blob) {
3500
+ const fallback = { width: 400, height: 300 };
3501
+ return new Promise((resolve) => {
3502
+ if (typeof Image === "undefined" || typeof URL.createObjectURL !== "function") return resolve(fallback);
3503
+ const url = URL.createObjectURL(blob);
3504
+ const img = new Image();
3505
+ const done = (size) => {
3506
+ URL.revokeObjectURL(url);
3507
+ resolve(size);
3508
+ };
3509
+ img.onload = () => done({ width: img.naturalWidth || 400, height: img.naturalHeight || 300 });
3510
+ img.onerror = () => done(fallback);
3511
+ img.src = url;
1762
3512
  });
1763
- return {
1764
- get: (id) => records.get(id),
1765
- list: () => [...records.values()],
1766
- subscribe: (listener) => {
1767
- listeners.add(listener);
1768
- return () => {
1769
- listeners.delete(listener);
1770
- };
1771
- },
1772
- receive(incoming) {
1773
- const current = records.get(incoming.id);
1774
- if (!current) {
1775
- put(fresh2(incoming));
1776
- return "created";
1777
- }
1778
- const key = incomingKey(incoming);
1779
- if (current.lastIncomingKey === key) return "unchanged";
1780
- const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
1781
- const userEditedTitle = current.title !== current.baseTitle;
1782
- const title = userEditedTitle ? current.title : incoming.title;
1783
- const meta = {
1784
- documentType: incoming.documentType ?? current.documentType,
1785
- status: incoming.status ?? current.status
1786
- };
1787
- if (!userEditedBody) {
1788
- put({
1789
- ...current,
1790
- ...meta,
1791
- title,
1792
- baseTitle: incoming.title,
1793
- json: incoming.json,
1794
- baseJson: incoming.json,
1795
- lastIncomingKey: key,
1796
- revision: current.revision + 1,
1797
- origin: "external",
1798
- pending: null,
1799
- updatedAt: Date.now()
1800
- });
1801
- return "applied";
1802
- }
1803
- const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
1804
- if (conflicts === 0) {
1805
- put({
1806
- ...current,
1807
- ...meta,
1808
- title,
1809
- baseTitle: incoming.title,
1810
- json: doc,
1811
- baseJson: incoming.json,
1812
- lastIncomingKey: key,
1813
- revision: current.revision + 1,
1814
- origin: "external",
1815
- pending: null,
1816
- updatedAt: Date.now()
1817
- });
1818
- return "merged";
1819
- }
1820
- put({
1821
- ...current,
1822
- ...meta,
1823
- lastIncomingKey: key,
1824
- pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
1825
- });
1826
- return "conflict";
1827
- },
1828
- hydrate(doc) {
1829
- put(fresh2(doc));
1830
- },
1831
- edit(id, json, origin) {
1832
- const current = records.get(id);
1833
- if (!current || docKey(current.json) === docKey(json)) return;
1834
- put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
1835
- },
1836
- setTitle(id, title) {
1837
- const current = records.get(id);
1838
- if (!current || current.title === title) return;
1839
- put({ ...current, title, updatedAt: Date.now() });
1840
- },
1841
- resolve(id, choice) {
1842
- const current = records.get(id);
1843
- const pending = current?.pending;
1844
- if (!current || !pending) return;
1845
- if (choice === "keep") {
1846
- put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
1847
- return;
1848
- }
1849
- const json = choice === "merge" ? pending.merged : pending.json;
1850
- const title = choice === "merge" ? current.title : pending.title;
1851
- put({
1852
- ...current,
1853
- title,
1854
- json,
1855
- baseJson: pending.json,
1856
- baseTitle: pending.title,
1857
- revision: current.revision + 1,
1858
- origin: "external",
1859
- pending: null,
1860
- updatedAt: Date.now()
1861
- });
1862
- },
1863
- remove(id) {
1864
- if (records.delete(id)) emit();
1865
- }
1866
- };
1867
- }
1868
- var defaultStore = null;
1869
- function getDefaultDocumentStore() {
1870
- if (!defaultStore) defaultStore = createDocumentStore();
1871
- return defaultStore;
1872
- }
1873
-
1874
- // src/components/document-editor/document-title.tsx
1875
- var import_jsx_runtime6 = require("react/jsx-runtime");
1876
- function DocumentTitle({ value, disabled, onChange }) {
1877
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1878
- "input",
1879
- {
1880
- className: "june-doc__title",
1881
- "aria-label": "Document title",
1882
- value,
1883
- disabled,
1884
- placeholder: "Untitled document",
1885
- onChange: (e) => onChange(e.target.value)
3513
+ }
3514
+ async function loadImage(src, attrs) {
3515
+ try {
3516
+ const res = await fetch(src);
3517
+ if (!res.ok) return null;
3518
+ const blob = await res.blob();
3519
+ const type = IMAGE_TYPES[blob.type.toLowerCase()];
3520
+ if (!type) return null;
3521
+ const natural = await naturalSize(blob);
3522
+ let width = Number(attrs?.width) || natural.width;
3523
+ let height = Number(attrs?.height) || natural.height * width / natural.width;
3524
+ if (width > MAX_IMAGE_WIDTH) {
3525
+ height = height * MAX_IMAGE_WIDTH / width;
3526
+ width = MAX_IMAGE_WIDTH;
1886
3527
  }
3528
+ return { data: new Uint8Array(await blob.arrayBuffer()), type, width: Math.round(width), height: Math.round(height) };
3529
+ } catch {
3530
+ return null;
3531
+ }
3532
+ }
3533
+ async function resolveImages(doc) {
3534
+ const map = /* @__PURE__ */ new Map();
3535
+ if (typeof fetch !== "function") return map;
3536
+ const found = collectImages(doc.content, /* @__PURE__ */ new Map());
3537
+ await Promise.all(
3538
+ [...found].map(async ([src, attrs]) => {
3539
+ const info = await loadImage(src, attrs);
3540
+ if (info) map.set(src, info);
3541
+ })
1887
3542
  );
3543
+ return map;
1888
3544
  }
1889
-
1890
- // src/components/document-editor/document-toolbar.tsx
1891
- var import_react8 = require("@tiptap/react");
1892
- var import_react9 = require("react");
1893
- init_document_utils();
1894
- var import_jsx_runtime7 = require("react/jsx-runtime");
1895
- var FORMAT_LABELS = {
1896
- html: "HTML",
1897
- markdown: "Markdown",
1898
- txt: "Plain text",
1899
- docx: "Word (.docx)",
1900
- pdf: "PDF (print)"
1901
- };
1902
- function Btn({ label, active, disabled, toggle, onClick, children }) {
1903
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1904
- "button",
1905
- {
1906
- type: "button",
1907
- className: "june-doc__btn",
1908
- "aria-label": label,
1909
- title: label,
1910
- "aria-pressed": toggle ? !!active : void 0,
1911
- "data-active": active || void 0,
1912
- disabled,
1913
- onMouseDown: (e) => e.preventDefault(),
1914
- onClick,
1915
- children
3545
+ function blocks(d, nodes, fmt, state) {
3546
+ const headings = [d.HeadingLevel.HEADING_1, d.HeadingLevel.HEADING_2, d.HeadingLevel.HEADING_3];
3547
+ const out = [];
3548
+ for (const node of nodes ?? []) {
3549
+ switch (node.type) {
3550
+ case "paragraph":
3551
+ out.push(paragraph(d, node, fmt));
3552
+ break;
3553
+ case "heading": {
3554
+ const level = Math.min(Math.max(Number(node.attrs?.level) || 1, 1), 3);
3555
+ out.push(paragraph(d, node, fmt, { heading: headings[level - 1] }));
3556
+ break;
3557
+ }
3558
+ case "bulletList":
3559
+ case "orderedList":
3560
+ out.push(...list(d, node, fmt, 0, state));
3561
+ break;
3562
+ case "blockquote":
3563
+ out.push(...blocks(d, node.content, { ...fmt, quote: true, indent: (fmt.indent ?? 0) + 360 }, state));
3564
+ break;
3565
+ case "codeBlock":
3566
+ out.push(code(d, node));
3567
+ break;
3568
+ case "horizontalRule":
3569
+ out.push(
3570
+ new d.Paragraph({
3571
+ border: { bottom: { color: "9CA3AF", space: 1, style: d.BorderStyle.SINGLE, size: 6 } },
3572
+ spacing: { after: 120 }
3573
+ })
3574
+ );
3575
+ break;
3576
+ case "table":
3577
+ out.push(table(d, node, state));
3578
+ break;
3579
+ case "pageBreak":
3580
+ out.push(new d.Paragraph({ children: [new d.PageBreak()] }));
3581
+ break;
3582
+ case "image":
3583
+ out.push(...image(d, node, state));
3584
+ break;
3585
+ default:
3586
+ if (node.content) out.push(...blocks(d, node.content, fmt, state));
1916
3587
  }
1917
- );
3588
+ }
3589
+ return out;
1918
3590
  }
1919
- function DocumentToolbar(props) {
1920
- const { editor, editable, exportFormats, canSave, saveState, onExport, onCopy, onSave } = props;
1921
- const [linkOpen, setLinkOpen] = (0, import_react9.useState)(false);
1922
- const [linkValue, setLinkValue] = (0, import_react9.useState)("");
1923
- const s = (0, import_react8.useEditorState)({
1924
- editor,
1925
- selector: ({ editor: ed }) => ed ? {
1926
- bold: ed.isActive("bold"),
1927
- italic: ed.isActive("italic"),
1928
- underline: ed.isActive("underline"),
1929
- strike: ed.isActive("strike"),
1930
- bullet: ed.isActive("bulletList"),
1931
- ordered: ed.isActive("orderedList"),
1932
- quote: ed.isActive("blockquote"),
1933
- link: ed.isActive("link"),
1934
- linkHref: ed.getAttributes("link").href ?? "",
1935
- h1: ed.isActive("heading", { level: 1 }),
1936
- h2: ed.isActive("heading", { level: 2 }),
1937
- h3: ed.isActive("heading", { level: 3 }),
1938
- left: ed.isActive({ textAlign: "left" }),
1939
- center: ed.isActive({ textAlign: "center" }),
1940
- right: ed.isActive({ textAlign: "right" }),
1941
- justify: ed.isActive({ textAlign: "justify" }),
1942
- inTable: ed.isActive("table"),
1943
- canUndo: ed.can().undo(),
1944
- canRedo: ed.can().redo()
1945
- } : null
1946
- });
1947
- const run = () => editor?.chain().focus();
1948
- const off = !editor || !editable;
1949
- const heading = s?.h1 ? "1" : s?.h2 ? "2" : s?.h3 ? "3" : "p";
1950
- const openLink = () => {
1951
- setLinkValue(s?.linkHref ?? "");
1952
- setLinkOpen(true);
1953
- };
1954
- const applyLink = () => {
1955
- const href = sanitizeHref(linkValue);
1956
- if (href) run()?.extendMarkRange("link").setLink({ href }).run();
1957
- setLinkOpen(false);
1958
- };
1959
- const removeLink = () => {
1960
- run()?.extendMarkRange("link").unsetLink().run();
1961
- setLinkOpen(false);
1962
- };
1963
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__toolbar", role: "toolbar", "aria-label": "Document formatting", children: [
1964
- editable ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1965
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", children: [
1966
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Undo", disabled: off || !s?.canUndo, onClick: () => run()?.undo().run(), children: "\u21B6" }),
1967
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Redo", disabled: off || !s?.canRedo, onClick: () => run()?.redo().run(), children: "\u21B7" })
1968
- ] }),
1969
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "june-doc__group", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1970
- "select",
1971
- {
1972
- className: "june-doc__select",
1973
- "aria-label": "Text style",
1974
- disabled: off,
1975
- value: heading,
1976
- onChange: (e) => {
1977
- const v = e.target.value;
1978
- if (v === "p") run()?.setParagraph().run();
1979
- else run()?.toggleHeading({ level: Number(v) }).run();
1980
- },
1981
- children: [
1982
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "p", children: "Paragraph" }),
1983
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "1", children: "Heading 1" }),
1984
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "2", children: "Heading 2" }),
1985
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "3", children: "Heading 3" })
1986
- ]
1987
- }
1988
- ) }),
1989
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", children: [
1990
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Bold", toggle: true, active: s?.bold, disabled: off, onClick: () => run()?.toggleBold().run(), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: "B" }) }),
1991
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Italic", toggle: true, active: s?.italic, disabled: off, onClick: () => run()?.toggleItalic().run(), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("i", { children: "I" }) }),
1992
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Underline", toggle: true, active: s?.underline, disabled: off, onClick: () => run()?.toggleUnderline().run(), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("u", { children: "U" }) }),
1993
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Strikethrough", toggle: true, active: s?.strike, disabled: off, onClick: () => run()?.toggleStrike().run(), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("s", { children: "S" }) })
1994
- ] }),
1995
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", children: [
1996
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Bullet list", toggle: true, active: s?.bullet, disabled: off, onClick: () => run()?.toggleBulletList().run(), children: "\u2022 List" }),
1997
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Numbered list", toggle: true, active: s?.ordered, disabled: off, onClick: () => run()?.toggleOrderedList().run(), children: "1. List" }),
1998
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Blockquote", toggle: true, active: s?.quote, disabled: off, onClick: () => run()?.toggleBlockquote().run(), children: "Quote" }),
1999
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Link", toggle: true, active: s?.link, disabled: off, onClick: openLink, children: "Link" })
2000
- ] }),
2001
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", children: [
2002
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align left", toggle: true, active: s?.left, disabled: off, onClick: () => run()?.setTextAlign("left").run(), children: "L" }),
2003
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align center", toggle: true, active: s?.center, disabled: off, onClick: () => run()?.setTextAlign("center").run(), children: "C" }),
2004
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Align right", toggle: true, active: s?.right, disabled: off, onClick: () => run()?.setTextAlign("right").run(), children: "R" }),
2005
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Justify", toggle: true, active: s?.justify, disabled: off, onClick: () => run()?.setTextAlign("justify").run(), children: "J" })
2006
- ] }),
2007
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group", children: [
2008
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Insert table", disabled: off, onClick: () => run()?.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: "Table" }),
2009
- s?.inTable ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2010
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Add row below", disabled: off, onClick: () => run()?.addRowAfter().run(), children: "+Row" }),
2011
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Add column right", disabled: off, onClick: () => run()?.addColumnAfter().run(), children: "+Col" }),
2012
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete row", disabled: off, onClick: () => run()?.deleteRow().run(), children: "\u2212Row" }),
2013
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete column", disabled: off, onClick: () => run()?.deleteColumn().run(), children: "\u2212Col" }),
2014
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Delete table", disabled: off, onClick: () => run()?.deleteTable().run(), children: "Del table" })
2015
- ] }) : null
2016
- ] })
2017
- ] }) : null,
2018
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__group june-doc__group--end", children: [
2019
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Copy document", onClick: onCopy, children: "Copy" }),
2020
- exportFormats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2021
- "select",
2022
- {
2023
- className: "june-doc__select",
2024
- "aria-label": "Export document",
2025
- value: "",
2026
- onChange: (e) => e.target.value && onExport(e.target.value),
2027
- children: [
2028
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: "Export\u2026" }),
2029
- exportFormats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: f, children: FORMAT_LABELS[f] ?? f }, f))
2030
- ]
2031
- }
2032
- ) : null,
2033
- canSave ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Btn, { label: "Save", disabled: saveState === "saving" || saveState === "saved" || saveState === "idle", onClick: onSave, children: "Save" }) : null
2034
- ] }),
2035
- linkOpen ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "june-doc__linkbar", children: [
2036
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2037
- "input",
3591
+ var exportDocx = async (ctx) => {
3592
+ const d = await import("docx");
3593
+ const state = { instance: 0, images: await resolveImages(ctx.json) };
3594
+ const children = [
3595
+ new d.Paragraph({ text: ctx.title, heading: d.HeadingLevel.TITLE }),
3596
+ ...blocks(d, ctx.json.content, {}, state)
3597
+ ];
3598
+ const doc = new d.Document({
3599
+ title: ctx.title,
3600
+ numbering: {
3601
+ config: [
2038
3602
  {
2039
- "aria-label": "Link address",
2040
- className: "june-doc__input",
2041
- placeholder: "https://example.com",
2042
- value: linkValue,
2043
- autoFocus: true,
2044
- onChange: (e) => setLinkValue(e.target.value),
2045
- onKeyDown: (e) => {
2046
- if (e.key === "Enter") applyLink();
2047
- if (e.key === "Escape") setLinkOpen(false);
2048
- }
3603
+ reference: ORDERED_REF,
3604
+ levels: [0, 1, 2].map((level) => ({
3605
+ level,
3606
+ format: d.LevelFormat.DECIMAL,
3607
+ text: `%${level + 1}.`,
3608
+ alignment: d.AlignmentType.START,
3609
+ style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
3610
+ }))
2049
3611
  }
2050
- ),
2051
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn", onClick: applyLink, children: "Apply" }),
2052
- s?.link ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn", onClick: removeLink, children: "Remove" }) : null,
2053
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => setLinkOpen(false), children: "Cancel" })
2054
- ] }) : null
2055
- ] });
3612
+ ]
3613
+ },
3614
+ sections: [{ children }]
3615
+ });
3616
+ const blob = await d.Packer.toBlob(doc);
3617
+ return { filename: `${ctx.basename}.docx`, mimeType: DOCX_MIME, blob };
3618
+ };
3619
+ function printHtml(html) {
3620
+ return new Promise((resolve, reject) => {
3621
+ const frame = document.createElement("iframe");
3622
+ frame.setAttribute("aria-hidden", "true");
3623
+ frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
3624
+ const cleanup = () => setTimeout(() => frame.remove(), 500);
3625
+ frame.onload = () => {
3626
+ try {
3627
+ const win = frame.contentWindow;
3628
+ if (!win) throw new Error("The print frame is unavailable");
3629
+ win.addEventListener("afterprint", cleanup);
3630
+ win.focus();
3631
+ win.print();
3632
+ setTimeout(cleanup, 12e4);
3633
+ resolve();
3634
+ } catch (error) {
3635
+ cleanup();
3636
+ reject(error);
3637
+ }
3638
+ };
3639
+ frame.srcdoc = html;
3640
+ document.body.appendChild(frame);
3641
+ });
3642
+ }
3643
+ var exportPdf = async (ctx) => {
3644
+ const { docToHtml: docToHtml2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
3645
+ await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
3646
+ return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
3647
+ };
3648
+ var registry = /* @__PURE__ */ new Map([
3649
+ ["html", exportHtml],
3650
+ ["markdown", exportMarkdown],
3651
+ ["txt", exportTxt],
3652
+ ["docx", exportDocx],
3653
+ ["pdf", exportPdf]
3654
+ ]);
3655
+ function registerDocumentExporter(format, exporter) {
3656
+ registry.set(format, exporter);
3657
+ }
3658
+ function listDocumentExporters() {
3659
+ return [...registry.keys()];
3660
+ }
3661
+ async function exportDocument(format, doc) {
3662
+ const exporter = registry.get(format);
3663
+ if (!exporter) throw new JuneError("EXPORT_ERROR", `No exporter is registered for "${format}"`);
3664
+ try {
3665
+ return await exporter({ ...doc, basename: slugify(doc.title) || "document" });
3666
+ } catch (error) {
3667
+ throw toJuneError(error, "EXPORT_ERROR", `Exporting ${format} failed`);
3668
+ }
3669
+ }
3670
+ function downloadResult(result) {
3671
+ if (!result.blob) return;
3672
+ const url = URL.createObjectURL(result.blob);
3673
+ const link = document.createElement("a");
3674
+ link.href = url;
3675
+ link.download = result.filename;
3676
+ document.body.appendChild(link);
3677
+ link.click();
3678
+ link.remove();
3679
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
3680
+ }
3681
+ async function copyDocumentToClipboard(doc) {
3682
+ const { docToHtml: docToHtml2, docToText: docToText2 } = await Promise.resolve().then(() => (init_document_engine(), document_engine_exports));
3683
+ const html = docToHtml2(doc.json);
3684
+ const text = `${doc.title}
3685
+
3686
+ ${docToText2(doc.json)}`;
3687
+ try {
3688
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
3689
+ await navigator.clipboard.write([
3690
+ new ClipboardItem({
3691
+ "text/html": new Blob([html], { type: "text/html" }),
3692
+ "text/plain": new Blob([text], { type: "text/plain" })
3693
+ })
3694
+ ]);
3695
+ return true;
3696
+ }
3697
+ await navigator.clipboard.writeText(text);
3698
+ return true;
3699
+ } catch {
3700
+ return false;
3701
+ }
2056
3702
  }
2057
3703
 
3704
+ // src/components/document-editor/document-editor.tsx
3705
+ init_document_extensions();
3706
+
2058
3707
  // src/components/document-editor/document-types.ts
2059
3708
  var ALL_EXPORT_FORMATS = [
2060
3709
  "html",
@@ -2066,7 +3715,7 @@ var ALL_EXPORT_FORMATS = [
2066
3715
 
2067
3716
  // src/components/document-editor/document-editor.tsx
2068
3717
  init_document_utils();
2069
- var import_jsx_runtime8 = require("react/jsx-runtime");
3718
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2070
3719
  var snapshotOf = (r) => ({
2071
3720
  documentId: r.id,
2072
3721
  title: r.title,
@@ -2081,6 +3730,8 @@ var SAVE_LABEL = {
2081
3730
  saved: "Saved",
2082
3731
  error: "Save failed"
2083
3732
  };
3733
+ var ZOOM_STEPS = [0.5, 0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
3734
+ 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;
2084
3735
  function DocumentEditorImpl(props) {
2085
3736
  const {
2086
3737
  documentId,
@@ -2094,15 +3745,15 @@ function DocumentEditorImpl(props) {
2094
3745
  className
2095
3746
  } = props;
2096
3747
  const ctxOptions = useDocumentOptions();
2097
- const options = (0, import_react11.useMemo)(() => ({ ...ctxOptions, ...props.options }), [ctxOptions, props.options]);
2098
- const optionsRef = (0, import_react11.useRef)(options);
3748
+ const options = (0, import_react16.useMemo)(() => ({ ...ctxOptions, ...props.options }), [ctxOptions, props.options]);
3749
+ const optionsRef = (0, import_react16.useRef)(options);
2099
3750
  optionsRef.current = options;
2100
- const store = (0, import_react11.useMemo)(() => options.store ?? getDefaultDocumentStore(), [options.store]);
2101
- const instanceId = (0, import_react11.useId)();
3751
+ const store = (0, import_react16.useMemo)(() => options.store ?? getDefaultDocumentStore(), [options.store]);
3752
+ const instanceId = (0, import_react16.useId)();
2102
3753
  const { theme: contextTheme } = useJuneTheme();
2103
3754
  const resolvedTheme = theme === "auto" ? contextTheme : theme;
2104
3755
  const contentKey = typeof content === "string" ? content : content ? JSON.stringify(content) : "";
2105
- const parsed = (0, import_react11.useMemo)(() => {
3756
+ const parsed = (0, import_react16.useMemo)(() => {
2106
3757
  if (!contentKey) return { json: null, error: null };
2107
3758
  try {
2108
3759
  return { json: parseDocumentContent(contentKey), error: null };
@@ -2110,45 +3761,65 @@ function DocumentEditorImpl(props) {
2110
3761
  return { json: null, error: toJuneError(e, "DOCUMENT_ERROR") };
2111
3762
  }
2112
3763
  }, [contentKey]);
2113
- (0, import_react11.useEffect)(() => {
3764
+ (0, import_react16.useEffect)(() => {
2114
3765
  if (parsed.error) optionsRef.current.onError?.(parsed.error);
2115
3766
  }, [parsed.error]);
2116
3767
  const title = useDocumentValue(store, documentId, (r) => r?.title ?? titleProp);
2117
3768
  const pending = useDocumentValue(store, documentId, (r) => r?.pending ?? null);
2118
3769
  const words = useDocumentValue(store, documentId, (r) => r ? wordCountOf(r.json) : 0);
2119
3770
  const recordStatus = useDocumentValue(store, documentId, (r) => r?.status ?? status);
2120
- const [saveState, setSaveState] = (0, import_react11.useState)("idle");
2121
- const [notice, setNotice] = (0, import_react11.useState)(null);
2122
- const lastSavedKey = (0, import_react11.useRef)("");
2123
- const seenRevision = (0, import_react11.useRef)(store.get(documentId)?.revision ?? 0);
2124
- const readyRef = (0, import_react11.useRef)(false);
2125
- const flash = (0, import_react11.useCallback)((message) => {
3771
+ const [saveState, setSaveState] = (0, import_react16.useState)("idle");
3772
+ const [notice, setNotice] = (0, import_react16.useState)(null);
3773
+ const [fullscreen, setFullscreen] = (0, import_react16.useState)(false);
3774
+ const [expanded, setExpanded] = (0, import_react16.useState)(true);
3775
+ const [showMarks, setShowMarks] = (0, import_react16.useState)(false);
3776
+ const [layout, setLayout] = (0, import_react16.useState)("page");
3777
+ const [findOpen, setFindOpen] = (0, import_react16.useState)(false);
3778
+ const [zoom, setZoom] = (0, import_react16.useState)(1);
3779
+ const [metrics, setMetrics] = (0, import_react16.useState)({ page: 1, pages: 1 });
3780
+ (0, import_react16.useEffect)(() => {
3781
+ if (!fullscreen || typeof document === "undefined") return;
3782
+ const prev = document.body.style.overflow;
3783
+ document.body.style.overflow = "hidden";
3784
+ return () => {
3785
+ document.body.style.overflow = prev;
3786
+ };
3787
+ }, [fullscreen]);
3788
+ const lastSavedKey = (0, import_react16.useRef)("");
3789
+ const seenRevision = (0, import_react16.useRef)(store.get(documentId)?.revision ?? 0);
3790
+ const readyRef = (0, import_react16.useRef)(false);
3791
+ const flash = (0, import_react16.useCallback)((message) => {
2126
3792
  setNotice(message);
2127
3793
  }, []);
2128
- (0, import_react11.useEffect)(() => {
3794
+ (0, import_react16.useEffect)(() => {
2129
3795
  if (!notice) return;
2130
3796
  const t = setTimeout(() => setNotice(null), 3e3);
2131
3797
  return () => clearTimeout(t);
2132
3798
  }, [notice]);
2133
- const emitChange = (0, import_react11.useCallback)(
3799
+ const emitChange = (0, import_react16.useCallback)(
2134
3800
  (source) => {
2135
3801
  const record = store.get(documentId);
2136
3802
  if (record) optionsRef.current.onDocumentChange?.({ ...snapshotOf(record), source });
2137
3803
  },
2138
3804
  [store, documentId]
2139
3805
  );
2140
- const saveRef = (0, import_react11.useRef)(async () => {
3806
+ const saveRef = (0, import_react16.useRef)(async () => {
2141
3807
  });
2142
- const autosave = (0, import_react11.useMemo)(() => {
3808
+ const autosave = (0, import_react16.useMemo)(() => {
2143
3809
  const delay = options.autosaveDelay ?? 1500;
2144
3810
  return delay > 0 ? debounce(() => void saveRef.current(), delay) : null;
2145
3811
  }, [options.autosaveDelay]);
2146
- (0, import_react11.useEffect)(() => () => autosave?.flush(), [autosave]);
3812
+ (0, import_react16.useEffect)(() => () => autosave?.flush(), [autosave]);
2147
3813
  saveRef.current = async () => {
2148
3814
  const record = store.get(documentId);
2149
3815
  const onSave = optionsRef.current.onSave;
2150
- if (!record || !onSave) return;
3816
+ if (!record) return;
2151
3817
  const key = JSON.stringify([record.title, docKey(record.json)]);
3818
+ if (!onSave) {
3819
+ lastSavedKey.current = key;
3820
+ setSaveState("saved");
3821
+ return;
3822
+ }
2152
3823
  if (key === lastSavedKey.current) {
2153
3824
  setSaveState("saved");
2154
3825
  return;
@@ -2169,21 +3840,32 @@ function DocumentEditorImpl(props) {
2169
3840
  optionsRef.current.onError?.(toJuneError(error, "DOCUMENT_ERROR", "Saving the document failed"));
2170
3841
  }
2171
3842
  };
2172
- const markDirty = (0, import_react11.useCallback)(() => {
2173
- if (!optionsRef.current.onSave) return;
2174
- setSaveState("dirty");
2175
- autosave?.();
2176
- }, [autosave]);
2177
- const [initialJson] = (0, import_react11.useState)(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
2178
- const extensions = (0, import_react11.useMemo)(() => createDocumentExtensions(), []);
2179
- const onUpdateRef = (0, import_react11.useRef)(() => {
3843
+ const markDirty = (0, import_react16.useCallback)(
3844
+ (userEdit = true) => {
3845
+ if (!optionsRef.current.onSave) {
3846
+ if (userEdit) setSaveState("dirty");
3847
+ return;
3848
+ }
3849
+ setSaveState("dirty");
3850
+ autosave?.();
3851
+ },
3852
+ [autosave]
3853
+ );
3854
+ (0, import_react16.useEffect)(() => {
3855
+ if (saveState !== "saved" || options.onSave) return;
3856
+ const t = setTimeout(() => setSaveState((s) => s === "saved" ? "idle" : s), 2500);
3857
+ return () => clearTimeout(t);
3858
+ }, [saveState, options.onSave]);
3859
+ const [initialJson] = (0, import_react16.useState)(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
3860
+ const extensions = (0, import_react16.useMemo)(() => createDocumentExtensions(), []);
3861
+ const onUpdateRef = (0, import_react16.useRef)(() => {
2180
3862
  });
2181
3863
  onUpdateRef.current = (json) => {
2182
3864
  store.edit(documentId, json, instanceId);
2183
3865
  emitChange("user");
2184
3866
  markDirty();
2185
3867
  };
2186
- const editor = (0, import_react10.useEditor)({
3868
+ const editor = (0, import_react15.useEditor)({
2187
3869
  extensions,
2188
3870
  content: initialJson,
2189
3871
  editable,
@@ -2198,11 +3880,13 @@ function DocumentEditorImpl(props) {
2198
3880
  },
2199
3881
  onUpdate: ({ editor: ed }) => onUpdateRef.current(ed.getJSON())
2200
3882
  });
2201
- (0, import_react11.useEffect)(() => {
2202
- editor?.setEditable(editable);
3883
+ (0, import_react16.useEffect)(() => {
3884
+ if (!editor || editor.isEditable === editable) return;
3885
+ editor.setEditable(editable);
2203
3886
  }, [editor, editable]);
2204
- (0, import_react11.useEffect)(() => {
3887
+ (0, import_react16.useEffect)(() => {
2205
3888
  if (!parsed.json) return;
3889
+ if (isDocumentEcho(store, documentId, contentKey)) return;
2206
3890
  const outcome = store.receive({
2207
3891
  id: documentId,
2208
3892
  title: titleProp,
@@ -2212,7 +3896,7 @@ function DocumentEditorImpl(props) {
2212
3896
  });
2213
3897
  if (outcome === "applied" || outcome === "merged") {
2214
3898
  emitChange(outcome === "applied" ? "ai" : "merge");
2215
- markDirty();
3899
+ markDirty(false);
2216
3900
  } else if (outcome === "conflict") {
2217
3901
  const record = store.get(documentId);
2218
3902
  optionsRef.current.onConflict?.({
@@ -2222,7 +3906,7 @@ function DocumentEditorImpl(props) {
2222
3906
  });
2223
3907
  }
2224
3908
  }, [store, documentId, titleProp, parsed.json, documentType, status, emitChange, markDirty]);
2225
- (0, import_react11.useEffect)(() => {
3909
+ (0, import_react16.useEffect)(() => {
2226
3910
  if (!editor) return;
2227
3911
  const apply = () => {
2228
3912
  const record = store.get(documentId);
@@ -2239,7 +3923,7 @@ function DocumentEditorImpl(props) {
2239
3923
  apply();
2240
3924
  return store.subscribe(apply);
2241
3925
  }, [editor, store, documentId, instanceId]);
2242
- (0, import_react11.useEffect)(() => {
3926
+ (0, import_react16.useEffect)(() => {
2243
3927
  if (!editor || readyRef.current) return;
2244
3928
  const record = store.get(documentId);
2245
3929
  if (!record) return;
@@ -2283,67 +3967,115 @@ function DocumentEditorImpl(props) {
2283
3967
  };
2284
3968
  const formats = (exportFormats ?? [...ALL_EXPORT_FORMATS]).filter((f) => listDocumentExporters().includes(f));
2285
3969
  if (parsed.error && !store.get(documentId)) {
2286
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
2287
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
2288
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("details", { children: [
2289
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("summary", { children: "Show raw content" }),
2290
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("pre", { className: "june-doc__raw", children: contentKey })
3970
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
3971
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
3972
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("details", { children: [
3973
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("summary", { children: "Show raw content" }),
3974
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("pre", { className: "june-doc__raw", children: contentKey })
2291
3975
  ] })
2292
3976
  ] });
2293
3977
  }
2294
- if (!editor) return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocumentDraft, { title, state: "loading" });
3978
+ if (!editor) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DocumentDraft, { title, state: "loading" });
2295
3979
  const isDraft = recordStatus === "draft";
2296
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3980
+ const classes = ["june-doc", fullscreen ? "june-doc--fullscreen" : "", className ?? ""].filter(Boolean).join(" ");
3981
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2297
3982
  "div",
2298
3983
  {
2299
- className: className ? `june-doc ${className}` : "june-doc",
3984
+ className: classes,
2300
3985
  "data-theme": resolvedTheme,
2301
3986
  "data-status": recordStatus,
3987
+ role: fullscreen ? "dialog" : void 0,
3988
+ "aria-modal": fullscreen || void 0,
3989
+ "aria-label": fullscreen ? title : void 0,
2302
3990
  onKeyDown: (e) => {
2303
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s" && options.onSave) {
3991
+ const mod = e.ctrlKey || e.metaKey;
3992
+ if (mod && e.key.toLowerCase() === "s" && options.onSave) {
2304
3993
  e.preventDefault();
2305
3994
  void saveRef.current();
3995
+ } else if (mod && e.key.toLowerCase() === "f") {
3996
+ e.preventDefault();
3997
+ setFindOpen(true);
3998
+ } else if (e.key === "Escape" && fullscreen && !e.defaultPrevented) {
3999
+ setFullscreen(false);
2306
4000
  }
2307
4001
  },
2308
4002
  children: [
2309
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "june-doc__header", children: [
2310
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocumentTitle, { value: title, disabled: !editable, onChange: handleTitle }),
2311
- isDraft ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "june-doc__badge", children: "AI draft" }) : null
2312
- ] }),
2313
- pending ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "june-doc__banner", role: "status", children: [
2314
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { children: [
4003
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4004
+ DocumentHeader,
4005
+ {
4006
+ title,
4007
+ editable,
4008
+ isDraft,
4009
+ fullscreen,
4010
+ onToggleFullscreen: () => setFullscreen((v) => !v),
4011
+ onTitle: handleTitle,
4012
+ exportFormats: formats,
4013
+ canSave: !!options.onSave,
4014
+ showSave: !!options.onSave || editable && saveState !== "idle",
4015
+ saveState,
4016
+ onExport: (f) => void handleExport(f),
4017
+ onCopy: () => void handleCopy(),
4018
+ onSave: () => void saveRef.current()
4019
+ }
4020
+ ),
4021
+ pending ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "june-doc__banner", role: "status", children: [
4022
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
2315
4023
  "The assistant suggested changes that overlap your edits (",
2316
4024
  pending.conflicts,
2317
4025
  " ",
2318
4026
  pending.conflicts === 1 ? "section" : "sections",
2319
4027
  ")."
2320
4028
  ] }),
2321
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "june-doc__banner-actions", children: [
2322
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
2323
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
2324
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
4029
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "june-doc__banner-actions", children: [
4030
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
4031
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
4032
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
2325
4033
  ] })
2326
4034
  ] }) : null,
2327
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4035
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2328
4036
  DocumentToolbar,
2329
4037
  {
2330
4038
  editor,
2331
4039
  editable,
2332
- exportFormats: formats,
2333
- canSave: !!options.onSave,
2334
- saveState,
2335
- onExport: (f) => void handleExport(f),
2336
- onCopy: () => void handleCopy(),
2337
- onSave: () => void saveRef.current()
4040
+ expanded,
4041
+ onToggleExpanded: () => setExpanded((v) => !v),
4042
+ showMarks,
4043
+ onToggleMarks: () => setShowMarks((v) => !v),
4044
+ layout,
4045
+ onToggleLayout: () => setLayout((v) => v === "page" ? "fluid" : "page"),
4046
+ findOpen,
4047
+ onToggleFind: () => setFindOpen((v) => !v),
4048
+ onNotice: flash
2338
4049
  }
2339
4050
  ),
2340
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DocumentContent, { editor }),
2341
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "june-doc__footer", "aria-live": "polite", children: [
2342
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { children: [
2343
- words,
2344
- " words"
4051
+ findOpen ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DocumentFindBar, { editor, editable, onClose: () => setFindOpen(false), onNotice: flash }) : null,
4052
+ editable ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DocumentBubbleMenu, { editor }) : null,
4053
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DocumentContent, { editor, layout, zoom, showMarks, onMetrics: setMetrics }),
4054
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "june-doc__footer", children: [
4055
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "june-doc__stats", children: [
4056
+ layout === "page" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
4057
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
4058
+ "Page ",
4059
+ metrics.page,
4060
+ " of ",
4061
+ metrics.pages
4062
+ ] }),
4063
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "june-doc__stats-sep", "aria-hidden": "true" })
4064
+ ] }) : null,
4065
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
4066
+ words,
4067
+ " words"
4068
+ ] })
2345
4069
  ] }),
2346
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: notice ?? SAVE_LABEL[saveState] })
4070
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "june-doc__status", "aria-live": "polite", "data-state": notice ? "notice" : saveState, children: notice ?? SAVE_LABEL[saveState] }),
4071
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "june-doc__zoom", role: "group", "aria-label": "Zoom", children: [
4072
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("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__ */ (0, import_jsx_runtime13.jsx)(Icon, { name: "minus", size: 14 }) }),
4073
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("button", { type: "button", className: "june-doc__zoom-value", "aria-label": "Reset zoom", title: "Reset zoom", onClick: () => setZoom(1), children: [
4074
+ Math.round(zoom * 100),
4075
+ "%"
4076
+ ] }),
4077
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("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__ */ (0, import_jsx_runtime13.jsx)(Icon, { name: "plus", size: 14 }) })
4078
+ ] })
2347
4079
  ] })
2348
4080
  ]
2349
4081
  }
@@ -2351,7 +4083,7 @@ function DocumentEditorImpl(props) {
2351
4083
  }
2352
4084
 
2353
4085
  // src/components/document-editor/document-component.tsx
2354
- var import_jsx_runtime9 = require("react/jsx-runtime");
4086
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2355
4087
  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.';
2356
4088
  var documentProps = import_v4.z.object({
2357
4089
  documentId: import_v4.z.string().describe("Stable id. Reuse it when revising the same document."),
@@ -2365,12 +4097,12 @@ var documentProps = import_v4.z.object({
2365
4097
  function DocumentEditor(props) {
2366
4098
  const { isStreaming } = useJuneRuntime();
2367
4099
  if (props.generating || isStreaming) {
2368
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DocumentDraft, { title: props.title, state: "drafting" });
4100
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocumentDraft, { title: props.title, state: "drafting" });
2369
4101
  }
2370
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DocumentEditorImpl, { ...props });
4102
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocumentEditorImpl, { ...props });
2371
4103
  }
2372
4104
  function OpenUIDocument({ props }) {
2373
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4105
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2374
4106
  DocumentEditor,
2375
4107
  {
2376
4108
  documentId: props.documentId,
@@ -2390,11 +4122,15 @@ var JuneDocumentEditorComponent = (0, import_react_lang3.defineComponent)({
2390
4122
  component: OpenUIDocument
2391
4123
  });
2392
4124
  function withDocumentEditor(library) {
2393
- return library.extend({
2394
- components: {
2395
- add: [{ component: JuneDocumentEditorComponent, slots: ["Card"] }]
2396
- }
2397
- });
4125
+ try {
4126
+ return library.extend({
4127
+ components: {
4128
+ add: [{ component: JuneDocumentEditorComponent, slots: ["Card"] }]
4129
+ }
4130
+ });
4131
+ } catch {
4132
+ return library.extend({ components: { add: [JuneDocumentEditorComponent] } });
4133
+ }
2398
4134
  }
2399
4135
 
2400
4136
  // src/components/document-editor/document-instructions.ts
@@ -2454,9 +4190,9 @@ var import_react_ui = require("@openuidev/react-ui");
2454
4190
  var juneLibrary = withDocumentEditor(import_react_ui.openuiLibrary);
2455
4191
 
2456
4192
  // src/core/JuneUI.tsx
2457
- var import_jsx_runtime10 = require("react/jsx-runtime");
4193
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2458
4194
  function JuneUI({ library = juneLibrary, ...props }) {
2459
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(JuneRenderer, { library, ...props });
4195
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(JuneRenderer, { library, ...props });
2460
4196
  }
2461
4197
  // Annotate the CommonJS export names for ESM import in node:
2462
4198
  0 && (module.exports = {
@@ -2486,17 +4222,21 @@ function JuneUI({ library = juneLibrary, ...props }) {
2486
4222
  getDefaultDocumentStore,
2487
4223
  getDocumentEditorInstructions,
2488
4224
  getDocumentEditorRules,
4225
+ isDocumentEcho,
2489
4226
  isJuneError,
2490
4227
  jsonFieldExtractor,
2491
4228
  juneLibrary,
2492
4229
  lightTheme,
2493
4230
  listDocumentExporters,
2494
4231
  mergeDocuments,
4232
+ readDocumentFromProgram,
2495
4233
  registerDocumentExporter,
4234
+ rememberDocumentEcho,
2496
4235
  runAction,
2497
4236
  serializeDocumentsForAgent,
2498
4237
  sseToText,
2499
4238
  toJuneError,
4239
+ updateDocumentInProgram,
2500
4240
  useDocumentOptions,
2501
4241
  useJuneRuntime,
2502
4242
  useJuneStream,