@june-ai/june-ui 0.2.1 → 0.2.3
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/README.md +598 -31
- package/dist/chunk-HFHRSNIL.js +174 -0
- package/dist/document-engine-C6ILFMRP.js +12 -0
- package/dist/index.cjs +1475 -98
- package/dist/index.d.cts +254 -10
- package/dist/index.d.ts +254 -10
- package/dist/index.js +1214 -75
- package/dist/june.css +140 -13
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JuneError,
|
|
3
|
+
createDocumentExtensions,
|
|
4
|
+
debounce,
|
|
5
|
+
docKey,
|
|
6
|
+
docToHtml,
|
|
7
|
+
docToMarkdown,
|
|
8
|
+
emptyDoc,
|
|
9
|
+
isJuneError,
|
|
10
|
+
parseDocumentContent,
|
|
11
|
+
sanitizeHref,
|
|
12
|
+
slugify,
|
|
13
|
+
toJuneError,
|
|
14
|
+
wordCountOf
|
|
15
|
+
} from "./chunk-HFHRSNIL.js";
|
|
16
|
+
|
|
1
17
|
// src/renderer/renderer.tsx
|
|
2
18
|
import { Renderer } from "@openuidev/react-lang";
|
|
3
19
|
import {
|
|
@@ -7,28 +23,6 @@ import {
|
|
|
7
23
|
useRef
|
|
8
24
|
} from "react";
|
|
9
25
|
|
|
10
|
-
// src/errors/errors.ts
|
|
11
|
-
var JuneError = class _JuneError extends Error {
|
|
12
|
-
constructor(code, message, options = {}) {
|
|
13
|
-
super(message);
|
|
14
|
-
Object.setPrototypeOf(this, _JuneError.prototype);
|
|
15
|
-
this.name = "JuneError";
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.details = options.details;
|
|
18
|
-
if (options.cause !== void 0) {
|
|
19
|
-
this.cause = options.cause;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
function isJuneError(value) {
|
|
24
|
-
return value instanceof JuneError;
|
|
25
|
-
}
|
|
26
|
-
function toJuneError(error, fallbackCode, message) {
|
|
27
|
-
if (error instanceof JuneError) return error;
|
|
28
|
-
const text = message ?? (error instanceof Error ? error.message : String(error));
|
|
29
|
-
return new JuneError(fallbackCode, text, { cause: error });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
// src/actions/action-handler.ts
|
|
33
27
|
function firstString(...values) {
|
|
34
28
|
for (const v of values) if (typeof v === "string" && v.length > 0) return v;
|
|
@@ -91,21 +85,36 @@ function createActionHandler(options) {
|
|
|
91
85
|
};
|
|
92
86
|
}
|
|
93
87
|
|
|
94
|
-
// src/
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
// src/components/document-editor/document-context.ts
|
|
89
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
90
|
+
var JuneDocumentContext = createContext({});
|
|
91
|
+
function useDocumentOptions() {
|
|
92
|
+
return useContext(JuneDocumentContext);
|
|
93
|
+
}
|
|
94
|
+
function useDocumentValue(store, id, selector) {
|
|
95
|
+
const read = () => selector(store.get(id));
|
|
96
|
+
return useSyncExternalStore(store.subscribe, read, read);
|
|
97
|
+
}
|
|
99
98
|
|
|
100
99
|
// src/core/context.ts
|
|
101
|
-
import { createContext, useContext } from "react";
|
|
102
|
-
var JuneContext =
|
|
100
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
101
|
+
var JuneContext = createContext2({
|
|
103
102
|
mode: "light",
|
|
104
103
|
theme: "light"
|
|
105
104
|
});
|
|
106
105
|
function useJuneTheme() {
|
|
107
|
-
return
|
|
106
|
+
return useContext2(JuneContext);
|
|
108
107
|
}
|
|
108
|
+
var JuneRuntimeContext = createContext2({ isStreaming: false });
|
|
109
|
+
function useJuneRuntime() {
|
|
110
|
+
return useContext2(JuneRuntimeContext);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/themes/theme-provider.tsx
|
|
114
|
+
import {
|
|
115
|
+
useMemo,
|
|
116
|
+
useSyncExternalStore as useSyncExternalStore2
|
|
117
|
+
} from "react";
|
|
109
118
|
|
|
110
119
|
// src/themes/dark.ts
|
|
111
120
|
var darkTheme = {
|
|
@@ -113,9 +122,7 @@ var darkTheme = {
|
|
|
113
122
|
"--june-muted": "#9ca3af",
|
|
114
123
|
"--june-border": "#1f2937",
|
|
115
124
|
"--june-accent": "#818cf8",
|
|
116
|
-
"--june-danger": "#f87171"
|
|
117
|
-
"--june-radius": "8px",
|
|
118
|
-
"--june-font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
125
|
+
"--june-danger": "#f87171"
|
|
119
126
|
};
|
|
120
127
|
|
|
121
128
|
// src/themes/light.ts
|
|
@@ -124,9 +131,7 @@ var lightTheme = {
|
|
|
124
131
|
"--june-muted": "#6b7280",
|
|
125
132
|
"--june-border": "#e5e7eb",
|
|
126
133
|
"--june-accent": "#4f46e5",
|
|
127
|
-
"--june-danger": "#b91c1c"
|
|
128
|
-
"--june-radius": "8px",
|
|
129
|
-
"--june-font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif"
|
|
134
|
+
"--june-danger": "#b91c1c"
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
// src/themes/theme-provider.tsx
|
|
@@ -147,27 +152,37 @@ function getServerSnapshot() {
|
|
|
147
152
|
return "light";
|
|
148
153
|
}
|
|
149
154
|
function useResolvedTheme(mode) {
|
|
150
|
-
const system =
|
|
155
|
+
const system = useSyncExternalStore2(subscribe, getSnapshot, getServerSnapshot);
|
|
151
156
|
return mode === "system" ? system : mode;
|
|
152
157
|
}
|
|
153
158
|
function JuneThemeProvider({
|
|
154
|
-
mode
|
|
159
|
+
mode,
|
|
155
160
|
vars,
|
|
156
161
|
className,
|
|
157
162
|
style,
|
|
158
163
|
children
|
|
159
164
|
}) {
|
|
160
|
-
const
|
|
165
|
+
const explicit = mode !== void 0;
|
|
166
|
+
const resolved = useResolvedTheme(mode ?? "system");
|
|
161
167
|
const mergedStyle = useMemo(() => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
if (!explicit && !vars && !style) return void 0;
|
|
169
|
+
const base = explicit ? resolved === "dark" ? darkTheme : lightTheme : void 0;
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
...vars,
|
|
173
|
+
...explicit ? { colorScheme: resolved } : {},
|
|
174
|
+
...style
|
|
175
|
+
};
|
|
176
|
+
}, [explicit, resolved, vars, style]);
|
|
177
|
+
const context = useMemo(
|
|
178
|
+
() => ({ mode: mode ?? "system", theme: resolved }),
|
|
179
|
+
[mode, resolved]
|
|
180
|
+
);
|
|
166
181
|
return /* @__PURE__ */ jsx(JuneContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
|
|
167
182
|
"div",
|
|
168
183
|
{
|
|
169
184
|
className: className ? `june-ui-root ${className}` : "june-ui-root",
|
|
170
|
-
"data-june-theme": resolved,
|
|
185
|
+
"data-june-theme": explicit ? resolved : void 0,
|
|
171
186
|
style: mergedStyle,
|
|
172
187
|
children
|
|
173
188
|
}
|
|
@@ -626,7 +641,8 @@ function JuneRenderer(props) {
|
|
|
626
641
|
isStreaming = false,
|
|
627
642
|
mixedContent = true,
|
|
628
643
|
renderText,
|
|
629
|
-
|
|
644
|
+
repair = true,
|
|
645
|
+
theme,
|
|
630
646
|
themeVars,
|
|
631
647
|
className,
|
|
632
648
|
style,
|
|
@@ -636,28 +652,15 @@ function JuneRenderer(props) {
|
|
|
636
652
|
onAction,
|
|
637
653
|
onError,
|
|
638
654
|
onJuneError,
|
|
655
|
+
documents,
|
|
639
656
|
loadingFallback = null,
|
|
640
657
|
emptyFallback = null,
|
|
641
658
|
errorFallback,
|
|
642
659
|
resetKeys,
|
|
643
660
|
...rendererProps
|
|
644
661
|
} = props;
|
|
645
|
-
const latest = useRef({
|
|
646
|
-
|
|
647
|
-
confirmAction,
|
|
648
|
-
parseActionEvent,
|
|
649
|
-
onAction,
|
|
650
|
-
onError,
|
|
651
|
-
onJuneError
|
|
652
|
-
});
|
|
653
|
-
latest.current = {
|
|
654
|
-
actions,
|
|
655
|
-
confirmAction,
|
|
656
|
-
parseActionEvent,
|
|
657
|
-
onAction,
|
|
658
|
-
onError,
|
|
659
|
-
onJuneError
|
|
660
|
-
};
|
|
662
|
+
const latest = useRef({ actions, confirmAction, parseActionEvent, onAction, onError, onJuneError });
|
|
663
|
+
latest.current = { actions, confirmAction, parseActionEvent, onAction, onError, onJuneError };
|
|
661
664
|
const handleAction = useCallback((event) => {
|
|
662
665
|
const l = latest.current;
|
|
663
666
|
l.onAction?.(event);
|
|
@@ -671,17 +674,17 @@ function JuneRenderer(props) {
|
|
|
671
674
|
}
|
|
672
675
|
}, []);
|
|
673
676
|
const handleParserErrors = useCallback((errors) => {
|
|
674
|
-
const
|
|
677
|
+
const list2 = Array.isArray(errors) ? errors : [];
|
|
675
678
|
const l = latest.current;
|
|
676
|
-
l.onError?.(
|
|
677
|
-
const real =
|
|
679
|
+
l.onError?.(list2);
|
|
680
|
+
const real = list2.filter((e) => e?.severity !== "warning");
|
|
678
681
|
if (real.length > 0) {
|
|
679
682
|
l.onJuneError?.(
|
|
680
683
|
new JuneError("PARSE_ERROR", `OpenUI reported ${real.length} error(s)`, { details: real })
|
|
681
684
|
);
|
|
682
685
|
}
|
|
683
|
-
if (
|
|
684
|
-
console.warn("[june-ui] OpenUI reported errors:",
|
|
686
|
+
if (list2.length > 0 && !l.onError && !l.onJuneError) {
|
|
687
|
+
console.warn("[june-ui] OpenUI reported errors:", list2);
|
|
685
688
|
}
|
|
686
689
|
}, []);
|
|
687
690
|
const handleBoundaryError = useCallback((error, info) => {
|
|
@@ -692,21 +695,32 @@ function JuneRenderer(props) {
|
|
|
692
695
|
})
|
|
693
696
|
);
|
|
694
697
|
}, []);
|
|
698
|
+
const runtime = useMemo2(() => ({ isStreaming }), [isStreaming]);
|
|
699
|
+
const documentOptions = useMemo2(
|
|
700
|
+
() => ({
|
|
701
|
+
...documents,
|
|
702
|
+
onError: (error) => {
|
|
703
|
+
documents?.onError?.(error);
|
|
704
|
+
latest.current.onJuneError?.(error);
|
|
705
|
+
}
|
|
706
|
+
}),
|
|
707
|
+
[documents]
|
|
708
|
+
);
|
|
695
709
|
const parts = useMemo2(
|
|
696
710
|
() => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
|
|
697
711
|
[mixedContent, response, isStreaming]
|
|
698
712
|
);
|
|
699
713
|
const uiText = useMemo2(() => {
|
|
700
714
|
const raw = parts && !parts.pure ? parts.ui : response;
|
|
701
|
-
if (!mixedContent || isStreaming || typeof raw !== "string" || raw.length === 0) return raw;
|
|
715
|
+
if (!repair || !mixedContent || isStreaming || typeof raw !== "string" || raw.length === 0) return raw;
|
|
702
716
|
return normalizeProgram(raw);
|
|
703
|
-
}, [parts, response, mixedContent, isStreaming]);
|
|
717
|
+
}, [parts, response, mixedContent, isStreaming, repair]);
|
|
704
718
|
const hasUi = typeof uiText === "string" && uiText.trim().length > 0;
|
|
705
719
|
const before = parts?.before ?? [];
|
|
706
720
|
const after = parts?.after ?? [];
|
|
707
721
|
const hasText = before.length + after.length > 0;
|
|
708
722
|
const text = (value, index, key) => /* @__PURE__ */ jsx3(Fragment, { children: renderText ? renderText(value, index) : /* @__PURE__ */ jsx3("p", { className: "june-text", children: value }) }, key);
|
|
709
|
-
return /* @__PURE__ */ jsx3(JuneThemeProvider, { mode: theme, vars: themeVars, className, style, children: /* @__PURE__ */ jsxs(
|
|
723
|
+
return /* @__PURE__ */ jsx3(JuneThemeProvider, { mode: theme, vars: themeVars, className, style, children: /* @__PURE__ */ jsx3(JuneRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx3(JuneDocumentContext.Provider, { value: documentOptions, children: /* @__PURE__ */ jsxs(
|
|
710
724
|
JuneErrorBoundary,
|
|
711
725
|
{
|
|
712
726
|
fallback: errorFallback,
|
|
@@ -729,7 +743,7 @@ function JuneRenderer(props) {
|
|
|
729
743
|
!hasUi && !hasText && (isStreaming ? loadingFallback : emptyFallback)
|
|
730
744
|
]
|
|
731
745
|
}
|
|
732
|
-
) });
|
|
746
|
+
) }) }) });
|
|
733
747
|
}
|
|
734
748
|
|
|
735
749
|
// src/components/library.ts
|
|
@@ -1052,33 +1066,1158 @@ function useJuneStream(options = {}) {
|
|
|
1052
1066
|
);
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
|
-
// src/
|
|
1069
|
+
// src/components/document-editor/document-component.tsx
|
|
1070
|
+
import { defineComponent as defineComponent2 } from "@openuidev/react-lang";
|
|
1071
|
+
import { z } from "zod/v4";
|
|
1072
|
+
|
|
1073
|
+
// src/components/document-editor/document-draft.tsx
|
|
1074
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1075
|
+
function DocumentDraft({ title, words, state }) {
|
|
1076
|
+
if (state === "empty") {
|
|
1077
|
+
return /* @__PURE__ */ jsx4("div", { className: "june-doc june-doc--draft", role: "alert", children: /* @__PURE__ */ jsx4("p", { className: "june-doc__draft-text", children: "No document content was received." }) });
|
|
1078
|
+
}
|
|
1079
|
+
return /* @__PURE__ */ jsxs2("div", { className: "june-doc june-doc--draft", role: "status", "aria-live": "polite", children: [
|
|
1080
|
+
title ? /* @__PURE__ */ jsx4("p", { className: "june-doc__draft-title", children: title }) : null,
|
|
1081
|
+
/* @__PURE__ */ jsx4("p", { className: "june-doc__draft-text", children: state === "drafting" ? `Drafting document\u2026${words ? ` ${words} words so far` : ""}` : "Loading editor\u2026" }),
|
|
1082
|
+
/* @__PURE__ */ jsxs2("div", { className: "june-doc__pulse", "aria-hidden": "true", children: [
|
|
1083
|
+
/* @__PURE__ */ jsx4("span", {}),
|
|
1084
|
+
/* @__PURE__ */ jsx4("span", {}),
|
|
1085
|
+
/* @__PURE__ */ jsx4("span", {})
|
|
1086
|
+
] })
|
|
1087
|
+
] });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/components/document-editor/document-editor.tsx
|
|
1091
|
+
import { useEditor } from "@tiptap/react";
|
|
1092
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useId, useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
|
|
1093
|
+
|
|
1094
|
+
// src/components/document-editor/document-content.tsx
|
|
1095
|
+
import { EditorContent } from "@tiptap/react";
|
|
1096
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1097
|
+
function DocumentContent({ editor }) {
|
|
1098
|
+
return /* @__PURE__ */ jsx5("div", { className: "june-doc__content", children: /* @__PURE__ */ jsx5(EditorContent, { editor }) });
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/components/document-editor/document-export.ts
|
|
1102
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1103
|
+
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}}';
|
|
1104
|
+
function buildHtmlDocument(title, bodyHtml) {
|
|
1105
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(
|
|
1106
|
+
title
|
|
1107
|
+
)}</title><style>${PRINT_CSS}</style></head><body><h1>${escapeHtml(title)}</h1>${bodyHtml}</body></html>`;
|
|
1108
|
+
}
|
|
1109
|
+
var textResult = (text, filename, mimeType) => ({
|
|
1110
|
+
filename,
|
|
1111
|
+
mimeType,
|
|
1112
|
+
text,
|
|
1113
|
+
blob: new Blob([text], { type: `${mimeType};charset=utf-8` })
|
|
1114
|
+
});
|
|
1115
|
+
var exportHtml = async (ctx) => {
|
|
1116
|
+
const { docToHtml: docToHtml2 } = await import("./document-engine-C6ILFMRP.js");
|
|
1117
|
+
return textResult(
|
|
1118
|
+
buildHtmlDocument(ctx.title, docToHtml2(ctx.json)),
|
|
1119
|
+
`${ctx.basename}.html`,
|
|
1120
|
+
"text/html"
|
|
1121
|
+
);
|
|
1122
|
+
};
|
|
1123
|
+
var exportMarkdown = async (ctx) => {
|
|
1124
|
+
const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-C6ILFMRP.js");
|
|
1125
|
+
return textResult(
|
|
1126
|
+
`# ${ctx.title}
|
|
1127
|
+
|
|
1128
|
+
${docToMarkdown2(ctx.json)}
|
|
1129
|
+
`,
|
|
1130
|
+
`${ctx.basename}.md`,
|
|
1131
|
+
"text/markdown"
|
|
1132
|
+
);
|
|
1133
|
+
};
|
|
1134
|
+
var exportTxt = async (ctx) => {
|
|
1135
|
+
const { docToText } = await import("./document-engine-C6ILFMRP.js");
|
|
1136
|
+
return textResult(`${ctx.title}
|
|
1137
|
+
|
|
1138
|
+
${docToText(ctx.json)}
|
|
1139
|
+
`, `${ctx.basename}.txt`, "text/plain");
|
|
1140
|
+
};
|
|
1141
|
+
var ORDERED_REF = "june-ordered";
|
|
1142
|
+
var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
1143
|
+
function runs(d, node, fmt) {
|
|
1144
|
+
const out = [];
|
|
1145
|
+
for (const child of node.content ?? []) {
|
|
1146
|
+
if (child.type === "hardBreak") {
|
|
1147
|
+
out.push(new d.TextRun({ break: 1 }));
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (child.type !== "text" || !child.text) continue;
|
|
1151
|
+
const marks = child.marks ?? [];
|
|
1152
|
+
const has = (type) => marks.some((m) => m.type === type);
|
|
1153
|
+
const link = marks.find((m) => m.type === "link");
|
|
1154
|
+
const run = new d.TextRun({
|
|
1155
|
+
text: child.text,
|
|
1156
|
+
bold: fmt.bold || has("bold") || void 0,
|
|
1157
|
+
italics: has("italic") || void 0,
|
|
1158
|
+
underline: has("underline") || link ? {} : void 0,
|
|
1159
|
+
strike: has("strike") || void 0,
|
|
1160
|
+
font: has("code") ? "Courier New" : void 0,
|
|
1161
|
+
color: link ? "0563C1" : void 0
|
|
1162
|
+
});
|
|
1163
|
+
const href = typeof link?.attrs?.href === "string" ? sanitizeHref(link.attrs.href) : null;
|
|
1164
|
+
out.push(href ? new d.ExternalHyperlink({ link: href, children: [run] }) : run);
|
|
1165
|
+
}
|
|
1166
|
+
return out;
|
|
1167
|
+
}
|
|
1168
|
+
function paragraph(d, node, fmt, extra = {}) {
|
|
1169
|
+
const align = node.attrs?.textAlign;
|
|
1170
|
+
return new d.Paragraph({
|
|
1171
|
+
children: runs(d, node, fmt),
|
|
1172
|
+
alignment: align === "center" ? d.AlignmentType.CENTER : align === "right" ? d.AlignmentType.RIGHT : align === "justify" ? d.AlignmentType.JUSTIFIED : void 0,
|
|
1173
|
+
indent: fmt.indent ? { left: fmt.indent } : void 0,
|
|
1174
|
+
border: fmt.quote ? { left: { color: "9CA3AF", space: 8, style: d.BorderStyle.SINGLE, size: 12 } } : void 0,
|
|
1175
|
+
spacing: { after: 120 },
|
|
1176
|
+
...extra
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function list(d, node, fmt, level, state) {
|
|
1180
|
+
const ordered = node.type === "orderedList";
|
|
1181
|
+
const instance = ordered ? ++state.instance : 0;
|
|
1182
|
+
const out = [];
|
|
1183
|
+
for (const item of node.content ?? []) {
|
|
1184
|
+
let first = true;
|
|
1185
|
+
for (const child of item.content ?? []) {
|
|
1186
|
+
if (child.type === "bulletList" || child.type === "orderedList") {
|
|
1187
|
+
out.push(...list(d, child, fmt, level + 1, state));
|
|
1188
|
+
} else if (child.type === "paragraph" && first) {
|
|
1189
|
+
first = false;
|
|
1190
|
+
const lvl = Math.min(level, 2);
|
|
1191
|
+
out.push(
|
|
1192
|
+
paragraph(
|
|
1193
|
+
d,
|
|
1194
|
+
child,
|
|
1195
|
+
fmt,
|
|
1196
|
+
ordered ? { numbering: { reference: ORDERED_REF, level: lvl, instance } } : { bullet: { level: lvl } }
|
|
1197
|
+
)
|
|
1198
|
+
);
|
|
1199
|
+
} else {
|
|
1200
|
+
out.push(...blocks(d, [child], { ...fmt, indent: (fmt.indent ?? 0) + 720 * (level + 1) }, state));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return out;
|
|
1205
|
+
}
|
|
1206
|
+
function code(d, node) {
|
|
1207
|
+
const lines = (node.content ?? []).map((c) => c.text ?? "").join("").split("\n");
|
|
1208
|
+
return new d.Paragraph({
|
|
1209
|
+
children: lines.map((line, i) => new d.TextRun({ text: line, font: "Courier New", break: i ? 1 : void 0 })),
|
|
1210
|
+
spacing: { after: 120 },
|
|
1211
|
+
shading: { type: d.ShadingType.CLEAR, fill: "F3F4F6" }
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
function table(d, node, state) {
|
|
1215
|
+
const rows = (node.content ?? []).map(
|
|
1216
|
+
(row) => new d.TableRow({
|
|
1217
|
+
children: (row.content ?? []).map((cell) => {
|
|
1218
|
+
const header = cell.type === "tableHeader";
|
|
1219
|
+
const children = blocks(d, cell.content, { bold: header }, state);
|
|
1220
|
+
return new d.TableCell({
|
|
1221
|
+
children: children.length ? children : [new d.Paragraph("")],
|
|
1222
|
+
shading: header ? { type: d.ShadingType.CLEAR, fill: "F3F4F6" } : void 0
|
|
1223
|
+
});
|
|
1224
|
+
})
|
|
1225
|
+
})
|
|
1226
|
+
);
|
|
1227
|
+
if (rows.length === 0) return new d.Paragraph("");
|
|
1228
|
+
return new d.Table({ rows, width: { size: 100, type: d.WidthType.PERCENTAGE } });
|
|
1229
|
+
}
|
|
1230
|
+
function blocks(d, nodes, fmt, state) {
|
|
1231
|
+
const headings = [d.HeadingLevel.HEADING_1, d.HeadingLevel.HEADING_2, d.HeadingLevel.HEADING_3];
|
|
1232
|
+
const out = [];
|
|
1233
|
+
for (const node of nodes ?? []) {
|
|
1234
|
+
switch (node.type) {
|
|
1235
|
+
case "paragraph":
|
|
1236
|
+
out.push(paragraph(d, node, fmt));
|
|
1237
|
+
break;
|
|
1238
|
+
case "heading": {
|
|
1239
|
+
const level = Math.min(Math.max(Number(node.attrs?.level) || 1, 1), 3);
|
|
1240
|
+
out.push(paragraph(d, node, fmt, { heading: headings[level - 1] }));
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
case "bulletList":
|
|
1244
|
+
case "orderedList":
|
|
1245
|
+
out.push(...list(d, node, fmt, 0, state));
|
|
1246
|
+
break;
|
|
1247
|
+
case "blockquote":
|
|
1248
|
+
out.push(...blocks(d, node.content, { ...fmt, quote: true, indent: (fmt.indent ?? 0) + 360 }, state));
|
|
1249
|
+
break;
|
|
1250
|
+
case "codeBlock":
|
|
1251
|
+
out.push(code(d, node));
|
|
1252
|
+
break;
|
|
1253
|
+
case "horizontalRule":
|
|
1254
|
+
out.push(
|
|
1255
|
+
new d.Paragraph({
|
|
1256
|
+
border: { bottom: { color: "9CA3AF", space: 1, style: d.BorderStyle.SINGLE, size: 6 } },
|
|
1257
|
+
spacing: { after: 120 }
|
|
1258
|
+
})
|
|
1259
|
+
);
|
|
1260
|
+
break;
|
|
1261
|
+
case "table":
|
|
1262
|
+
out.push(table(d, node, state));
|
|
1263
|
+
break;
|
|
1264
|
+
default:
|
|
1265
|
+
if (node.content) out.push(...blocks(d, node.content, fmt, state));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return out;
|
|
1269
|
+
}
|
|
1270
|
+
var exportDocx = async (ctx) => {
|
|
1271
|
+
const d = await import("docx");
|
|
1272
|
+
const state = { instance: 0 };
|
|
1273
|
+
const children = [
|
|
1274
|
+
new d.Paragraph({ text: ctx.title, heading: d.HeadingLevel.TITLE }),
|
|
1275
|
+
...blocks(d, ctx.json.content, {}, state)
|
|
1276
|
+
];
|
|
1277
|
+
const doc = new d.Document({
|
|
1278
|
+
title: ctx.title,
|
|
1279
|
+
numbering: {
|
|
1280
|
+
config: [
|
|
1281
|
+
{
|
|
1282
|
+
reference: ORDERED_REF,
|
|
1283
|
+
levels: [0, 1, 2].map((level) => ({
|
|
1284
|
+
level,
|
|
1285
|
+
format: d.LevelFormat.DECIMAL,
|
|
1286
|
+
text: `%${level + 1}.`,
|
|
1287
|
+
alignment: d.AlignmentType.START,
|
|
1288
|
+
style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } }
|
|
1289
|
+
}))
|
|
1290
|
+
}
|
|
1291
|
+
]
|
|
1292
|
+
},
|
|
1293
|
+
sections: [{ children }]
|
|
1294
|
+
});
|
|
1295
|
+
const blob = await d.Packer.toBlob(doc);
|
|
1296
|
+
return { filename: `${ctx.basename}.docx`, mimeType: DOCX_MIME, blob };
|
|
1297
|
+
};
|
|
1298
|
+
function printHtml(html) {
|
|
1299
|
+
return new Promise((resolve, reject) => {
|
|
1300
|
+
const frame = document.createElement("iframe");
|
|
1301
|
+
frame.setAttribute("aria-hidden", "true");
|
|
1302
|
+
frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
|
|
1303
|
+
const cleanup = () => setTimeout(() => frame.remove(), 500);
|
|
1304
|
+
frame.onload = () => {
|
|
1305
|
+
try {
|
|
1306
|
+
const win = frame.contentWindow;
|
|
1307
|
+
if (!win) throw new Error("The print frame is unavailable");
|
|
1308
|
+
win.addEventListener("afterprint", cleanup);
|
|
1309
|
+
win.focus();
|
|
1310
|
+
win.print();
|
|
1311
|
+
setTimeout(cleanup, 12e4);
|
|
1312
|
+
resolve();
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
cleanup();
|
|
1315
|
+
reject(error);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
frame.srcdoc = html;
|
|
1319
|
+
document.body.appendChild(frame);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
var exportPdf = async (ctx) => {
|
|
1323
|
+
const { docToHtml: docToHtml2 } = await import("./document-engine-C6ILFMRP.js");
|
|
1324
|
+
await printHtml(buildHtmlDocument(ctx.title, docToHtml2(ctx.json)));
|
|
1325
|
+
return { filename: `${ctx.basename}.pdf`, mimeType: "application/pdf", handled: true };
|
|
1326
|
+
};
|
|
1327
|
+
var registry = /* @__PURE__ */ new Map([
|
|
1328
|
+
["html", exportHtml],
|
|
1329
|
+
["markdown", exportMarkdown],
|
|
1330
|
+
["txt", exportTxt],
|
|
1331
|
+
["docx", exportDocx],
|
|
1332
|
+
["pdf", exportPdf]
|
|
1333
|
+
]);
|
|
1334
|
+
function registerDocumentExporter(format, exporter) {
|
|
1335
|
+
registry.set(format, exporter);
|
|
1336
|
+
}
|
|
1337
|
+
function listDocumentExporters() {
|
|
1338
|
+
return [...registry.keys()];
|
|
1339
|
+
}
|
|
1340
|
+
async function exportDocument(format, doc) {
|
|
1341
|
+
const exporter = registry.get(format);
|
|
1342
|
+
if (!exporter) throw new JuneError("EXPORT_ERROR", `No exporter is registered for "${format}"`);
|
|
1343
|
+
try {
|
|
1344
|
+
return await exporter({ ...doc, basename: slugify(doc.title) || "document" });
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
throw toJuneError(error, "EXPORT_ERROR", `Exporting ${format} failed`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function downloadResult(result) {
|
|
1350
|
+
if (!result.blob) return;
|
|
1351
|
+
const url = URL.createObjectURL(result.blob);
|
|
1352
|
+
const link = document.createElement("a");
|
|
1353
|
+
link.href = url;
|
|
1354
|
+
link.download = result.filename;
|
|
1355
|
+
document.body.appendChild(link);
|
|
1356
|
+
link.click();
|
|
1357
|
+
link.remove();
|
|
1358
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
1359
|
+
}
|
|
1360
|
+
async function copyDocumentToClipboard(doc) {
|
|
1361
|
+
const { docToHtml: docToHtml2, docToText } = await import("./document-engine-C6ILFMRP.js");
|
|
1362
|
+
const html = docToHtml2(doc.json);
|
|
1363
|
+
const text = `${doc.title}
|
|
1364
|
+
|
|
1365
|
+
${docToText(doc.json)}`;
|
|
1366
|
+
try {
|
|
1367
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
1368
|
+
await navigator.clipboard.write([
|
|
1369
|
+
new ClipboardItem({
|
|
1370
|
+
"text/html": new Blob([html], { type: "text/html" }),
|
|
1371
|
+
"text/plain": new Blob([text], { type: "text/plain" })
|
|
1372
|
+
})
|
|
1373
|
+
]);
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
await navigator.clipboard.writeText(text);
|
|
1377
|
+
return true;
|
|
1378
|
+
} catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/components/document-editor/document-merge.ts
|
|
1384
|
+
var MAX_CELLS = 4e6;
|
|
1385
|
+
function matchIndices(a, b) {
|
|
1386
|
+
const n = a.length;
|
|
1387
|
+
const m = b.length;
|
|
1388
|
+
if (n * m > MAX_CELLS) return null;
|
|
1389
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
1390
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
1391
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
1392
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1396
|
+
let i = 0;
|
|
1397
|
+
let j = 0;
|
|
1398
|
+
while (i < n && j < m) {
|
|
1399
|
+
if (a[i] === b[j]) {
|
|
1400
|
+
matches.set(i, j);
|
|
1401
|
+
i++;
|
|
1402
|
+
j++;
|
|
1403
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
|
|
1404
|
+
else j++;
|
|
1405
|
+
}
|
|
1406
|
+
return matches;
|
|
1407
|
+
}
|
|
1408
|
+
var same = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
|
|
1409
|
+
function mergeBlocks(base, local, incoming) {
|
|
1410
|
+
if (same(local, base)) return { blocks: incoming, conflicts: 0 };
|
|
1411
|
+
if (same(incoming, base) || same(local, incoming)) return { blocks: local, conflicts: 0 };
|
|
1412
|
+
const ml = matchIndices(base, local);
|
|
1413
|
+
const mn = matchIndices(base, incoming);
|
|
1414
|
+
if (!ml || !mn) return { blocks: null, conflicts: 1 };
|
|
1415
|
+
const anchors = [...ml.keys()].filter((i) => mn.has(i)).sort((x, y) => x - y);
|
|
1416
|
+
const out = [];
|
|
1417
|
+
let conflicts = 0;
|
|
1418
|
+
let pb = -1;
|
|
1419
|
+
let pl = -1;
|
|
1420
|
+
let pn = -1;
|
|
1421
|
+
const region = (bEnd, lEnd, nEnd) => {
|
|
1422
|
+
const B = base.slice(pb + 1, bEnd);
|
|
1423
|
+
const L = local.slice(pl + 1, lEnd);
|
|
1424
|
+
const N = incoming.slice(pn + 1, nEnd);
|
|
1425
|
+
if (B.length === L.length && L.length === N.length) {
|
|
1426
|
+
for (let k = 0; k < B.length; k++) {
|
|
1427
|
+
if (L[k] === B[k]) out.push(N[k]);
|
|
1428
|
+
else if (N[k] === B[k] || N[k] === L[k]) out.push(L[k]);
|
|
1429
|
+
else {
|
|
1430
|
+
conflicts++;
|
|
1431
|
+
out.push(L[k]);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (same(L, B)) out.push(...N);
|
|
1437
|
+
else if (same(N, B) || same(L, N)) out.push(...L);
|
|
1438
|
+
else {
|
|
1439
|
+
conflicts++;
|
|
1440
|
+
out.push(...L);
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
for (const a of anchors) {
|
|
1444
|
+
const la = ml.get(a);
|
|
1445
|
+
const na = mn.get(a);
|
|
1446
|
+
region(a, la, na);
|
|
1447
|
+
out.push(local[la]);
|
|
1448
|
+
pb = a;
|
|
1449
|
+
pl = la;
|
|
1450
|
+
pn = na;
|
|
1451
|
+
}
|
|
1452
|
+
region(base.length, local.length, incoming.length);
|
|
1453
|
+
return { blocks: out, conflicts };
|
|
1454
|
+
}
|
|
1455
|
+
function mergeDocuments(base, local, incoming) {
|
|
1456
|
+
const toBlocks = (d) => (d.content ?? []).map((node) => JSON.stringify(node));
|
|
1457
|
+
const { blocks: blocks2, conflicts } = mergeBlocks(toBlocks(base), toBlocks(local), toBlocks(incoming));
|
|
1458
|
+
if (!blocks2) return { doc: local, conflicts: Math.max(1, conflicts) };
|
|
1459
|
+
if (blocks2.length === 0) return { doc: emptyDoc(), conflicts };
|
|
1460
|
+
return { doc: { type: "doc", content: blocks2.map((b) => JSON.parse(b)) }, conflicts };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/components/document-editor/document-store.ts
|
|
1464
|
+
var incomingKey = (doc) => JSON.stringify([doc.title, docKey(doc.json)]);
|
|
1465
|
+
function createDocumentStore() {
|
|
1466
|
+
const records = /* @__PURE__ */ new Map();
|
|
1467
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1468
|
+
const emit = () => listeners.forEach((listener) => listener());
|
|
1469
|
+
const put = (record) => {
|
|
1470
|
+
records.set(record.id, record);
|
|
1471
|
+
emit();
|
|
1472
|
+
};
|
|
1473
|
+
const fresh2 = (doc) => ({
|
|
1474
|
+
id: doc.id,
|
|
1475
|
+
title: doc.title,
|
|
1476
|
+
documentType: doc.documentType,
|
|
1477
|
+
status: doc.status,
|
|
1478
|
+
json: doc.json,
|
|
1479
|
+
baseJson: doc.json,
|
|
1480
|
+
baseTitle: doc.title,
|
|
1481
|
+
lastIncomingKey: incomingKey(doc),
|
|
1482
|
+
revision: 0,
|
|
1483
|
+
origin: "external",
|
|
1484
|
+
pending: null,
|
|
1485
|
+
updatedAt: Date.now()
|
|
1486
|
+
});
|
|
1487
|
+
return {
|
|
1488
|
+
get: (id) => records.get(id),
|
|
1489
|
+
list: () => [...records.values()],
|
|
1490
|
+
subscribe: (listener) => {
|
|
1491
|
+
listeners.add(listener);
|
|
1492
|
+
return () => {
|
|
1493
|
+
listeners.delete(listener);
|
|
1494
|
+
};
|
|
1495
|
+
},
|
|
1496
|
+
receive(incoming) {
|
|
1497
|
+
const current = records.get(incoming.id);
|
|
1498
|
+
if (!current) {
|
|
1499
|
+
put(fresh2(incoming));
|
|
1500
|
+
return "created";
|
|
1501
|
+
}
|
|
1502
|
+
const key = incomingKey(incoming);
|
|
1503
|
+
if (current.lastIncomingKey === key) return "unchanged";
|
|
1504
|
+
const userEditedBody = docKey(current.json) !== docKey(current.baseJson);
|
|
1505
|
+
const userEditedTitle = current.title !== current.baseTitle;
|
|
1506
|
+
const title = userEditedTitle ? current.title : incoming.title;
|
|
1507
|
+
const meta = {
|
|
1508
|
+
documentType: incoming.documentType ?? current.documentType,
|
|
1509
|
+
status: incoming.status ?? current.status
|
|
1510
|
+
};
|
|
1511
|
+
if (!userEditedBody) {
|
|
1512
|
+
put({
|
|
1513
|
+
...current,
|
|
1514
|
+
...meta,
|
|
1515
|
+
title,
|
|
1516
|
+
baseTitle: incoming.title,
|
|
1517
|
+
json: incoming.json,
|
|
1518
|
+
baseJson: incoming.json,
|
|
1519
|
+
lastIncomingKey: key,
|
|
1520
|
+
revision: current.revision + 1,
|
|
1521
|
+
origin: "external",
|
|
1522
|
+
pending: null,
|
|
1523
|
+
updatedAt: Date.now()
|
|
1524
|
+
});
|
|
1525
|
+
return "applied";
|
|
1526
|
+
}
|
|
1527
|
+
const { doc, conflicts } = mergeDocuments(current.baseJson, current.json, incoming.json);
|
|
1528
|
+
if (conflicts === 0) {
|
|
1529
|
+
put({
|
|
1530
|
+
...current,
|
|
1531
|
+
...meta,
|
|
1532
|
+
title,
|
|
1533
|
+
baseTitle: incoming.title,
|
|
1534
|
+
json: doc,
|
|
1535
|
+
baseJson: incoming.json,
|
|
1536
|
+
lastIncomingKey: key,
|
|
1537
|
+
revision: current.revision + 1,
|
|
1538
|
+
origin: "external",
|
|
1539
|
+
pending: null,
|
|
1540
|
+
updatedAt: Date.now()
|
|
1541
|
+
});
|
|
1542
|
+
return "merged";
|
|
1543
|
+
}
|
|
1544
|
+
put({
|
|
1545
|
+
...current,
|
|
1546
|
+
...meta,
|
|
1547
|
+
lastIncomingKey: key,
|
|
1548
|
+
pending: { json: incoming.json, title: incoming.title, merged: doc, conflicts }
|
|
1549
|
+
});
|
|
1550
|
+
return "conflict";
|
|
1551
|
+
},
|
|
1552
|
+
hydrate(doc) {
|
|
1553
|
+
put(fresh2(doc));
|
|
1554
|
+
},
|
|
1555
|
+
edit(id, json, origin) {
|
|
1556
|
+
const current = records.get(id);
|
|
1557
|
+
if (!current || docKey(current.json) === docKey(json)) return;
|
|
1558
|
+
put({ ...current, json, origin, revision: current.revision + 1, updatedAt: Date.now() });
|
|
1559
|
+
},
|
|
1560
|
+
setTitle(id, title) {
|
|
1561
|
+
const current = records.get(id);
|
|
1562
|
+
if (!current || current.title === title) return;
|
|
1563
|
+
put({ ...current, title, updatedAt: Date.now() });
|
|
1564
|
+
},
|
|
1565
|
+
resolve(id, choice) {
|
|
1566
|
+
const current = records.get(id);
|
|
1567
|
+
const pending = current?.pending;
|
|
1568
|
+
if (!current || !pending) return;
|
|
1569
|
+
if (choice === "keep") {
|
|
1570
|
+
put({ ...current, baseJson: pending.json, baseTitle: pending.title, pending: null });
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const json = choice === "merge" ? pending.merged : pending.json;
|
|
1574
|
+
const title = choice === "merge" ? current.title : pending.title;
|
|
1575
|
+
put({
|
|
1576
|
+
...current,
|
|
1577
|
+
title,
|
|
1578
|
+
json,
|
|
1579
|
+
baseJson: pending.json,
|
|
1580
|
+
baseTitle: pending.title,
|
|
1581
|
+
revision: current.revision + 1,
|
|
1582
|
+
origin: "external",
|
|
1583
|
+
pending: null,
|
|
1584
|
+
updatedAt: Date.now()
|
|
1585
|
+
});
|
|
1586
|
+
},
|
|
1587
|
+
remove(id) {
|
|
1588
|
+
if (records.delete(id)) emit();
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
var defaultStore = null;
|
|
1593
|
+
function getDefaultDocumentStore() {
|
|
1594
|
+
if (!defaultStore) defaultStore = createDocumentStore();
|
|
1595
|
+
return defaultStore;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/components/document-editor/document-title.tsx
|
|
1599
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1600
|
+
function DocumentTitle({ value, disabled, onChange }) {
|
|
1601
|
+
return /* @__PURE__ */ jsx6(
|
|
1602
|
+
"input",
|
|
1603
|
+
{
|
|
1604
|
+
className: "june-doc__title",
|
|
1605
|
+
"aria-label": "Document title",
|
|
1606
|
+
value,
|
|
1607
|
+
disabled,
|
|
1608
|
+
placeholder: "Untitled document",
|
|
1609
|
+
onChange: (e) => onChange(e.target.value)
|
|
1610
|
+
}
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/components/document-editor/document-toolbar.tsx
|
|
1615
|
+
import { useEditorState } from "@tiptap/react";
|
|
1616
|
+
import { useState as useState2 } from "react";
|
|
1617
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1618
|
+
var FORMAT_LABELS = {
|
|
1619
|
+
html: "HTML",
|
|
1620
|
+
markdown: "Markdown",
|
|
1621
|
+
txt: "Plain text",
|
|
1622
|
+
docx: "Word (.docx)",
|
|
1623
|
+
pdf: "PDF (print)"
|
|
1624
|
+
};
|
|
1625
|
+
function Btn({ label, active, disabled, toggle, onClick, children }) {
|
|
1626
|
+
return /* @__PURE__ */ jsx7(
|
|
1627
|
+
"button",
|
|
1628
|
+
{
|
|
1629
|
+
type: "button",
|
|
1630
|
+
className: "june-doc__btn",
|
|
1631
|
+
"aria-label": label,
|
|
1632
|
+
title: label,
|
|
1633
|
+
"aria-pressed": toggle ? !!active : void 0,
|
|
1634
|
+
"data-active": active || void 0,
|
|
1635
|
+
disabled,
|
|
1636
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1637
|
+
onClick,
|
|
1638
|
+
children
|
|
1639
|
+
}
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
function DocumentToolbar(props) {
|
|
1643
|
+
const { editor, editable, exportFormats, canSave, saveState, onExport, onCopy, onSave } = props;
|
|
1644
|
+
const [linkOpen, setLinkOpen] = useState2(false);
|
|
1645
|
+
const [linkValue, setLinkValue] = useState2("");
|
|
1646
|
+
const s = useEditorState({
|
|
1647
|
+
editor,
|
|
1648
|
+
selector: ({ editor: ed }) => ed ? {
|
|
1649
|
+
bold: ed.isActive("bold"),
|
|
1650
|
+
italic: ed.isActive("italic"),
|
|
1651
|
+
underline: ed.isActive("underline"),
|
|
1652
|
+
strike: ed.isActive("strike"),
|
|
1653
|
+
bullet: ed.isActive("bulletList"),
|
|
1654
|
+
ordered: ed.isActive("orderedList"),
|
|
1655
|
+
quote: ed.isActive("blockquote"),
|
|
1656
|
+
link: ed.isActive("link"),
|
|
1657
|
+
linkHref: ed.getAttributes("link").href ?? "",
|
|
1658
|
+
h1: ed.isActive("heading", { level: 1 }),
|
|
1659
|
+
h2: ed.isActive("heading", { level: 2 }),
|
|
1660
|
+
h3: ed.isActive("heading", { level: 3 }),
|
|
1661
|
+
left: ed.isActive({ textAlign: "left" }),
|
|
1662
|
+
center: ed.isActive({ textAlign: "center" }),
|
|
1663
|
+
right: ed.isActive({ textAlign: "right" }),
|
|
1664
|
+
justify: ed.isActive({ textAlign: "justify" }),
|
|
1665
|
+
inTable: ed.isActive("table"),
|
|
1666
|
+
canUndo: ed.can().undo(),
|
|
1667
|
+
canRedo: ed.can().redo()
|
|
1668
|
+
} : null
|
|
1669
|
+
});
|
|
1670
|
+
const run = () => editor?.chain().focus();
|
|
1671
|
+
const off = !editor || !editable;
|
|
1672
|
+
const heading = s?.h1 ? "1" : s?.h2 ? "2" : s?.h3 ? "3" : "p";
|
|
1673
|
+
const openLink = () => {
|
|
1674
|
+
setLinkValue(s?.linkHref ?? "");
|
|
1675
|
+
setLinkOpen(true);
|
|
1676
|
+
};
|
|
1677
|
+
const applyLink = () => {
|
|
1678
|
+
const href = sanitizeHref(linkValue);
|
|
1679
|
+
if (href) run()?.extendMarkRange("link").setLink({ href }).run();
|
|
1680
|
+
setLinkOpen(false);
|
|
1681
|
+
};
|
|
1682
|
+
const removeLink = () => {
|
|
1683
|
+
run()?.extendMarkRange("link").unsetLink().run();
|
|
1684
|
+
setLinkOpen(false);
|
|
1685
|
+
};
|
|
1686
|
+
return /* @__PURE__ */ jsxs3("div", { className: "june-doc__toolbar", role: "toolbar", "aria-label": "Document formatting", children: [
|
|
1687
|
+
editable ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1688
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
|
|
1689
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Undo", disabled: off || !s?.canUndo, onClick: () => run()?.undo().run(), children: "\u21B6" }),
|
|
1690
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Redo", disabled: off || !s?.canRedo, onClick: () => run()?.redo().run(), children: "\u21B7" })
|
|
1691
|
+
] }),
|
|
1692
|
+
/* @__PURE__ */ jsx7("div", { className: "june-doc__group", children: /* @__PURE__ */ jsxs3(
|
|
1693
|
+
"select",
|
|
1694
|
+
{
|
|
1695
|
+
className: "june-doc__select",
|
|
1696
|
+
"aria-label": "Text style",
|
|
1697
|
+
disabled: off,
|
|
1698
|
+
value: heading,
|
|
1699
|
+
onChange: (e) => {
|
|
1700
|
+
const v = e.target.value;
|
|
1701
|
+
if (v === "p") run()?.setParagraph().run();
|
|
1702
|
+
else run()?.toggleHeading({ level: Number(v) }).run();
|
|
1703
|
+
},
|
|
1704
|
+
children: [
|
|
1705
|
+
/* @__PURE__ */ jsx7("option", { value: "p", children: "Paragraph" }),
|
|
1706
|
+
/* @__PURE__ */ jsx7("option", { value: "1", children: "Heading 1" }),
|
|
1707
|
+
/* @__PURE__ */ jsx7("option", { value: "2", children: "Heading 2" }),
|
|
1708
|
+
/* @__PURE__ */ jsx7("option", { value: "3", children: "Heading 3" })
|
|
1709
|
+
]
|
|
1710
|
+
}
|
|
1711
|
+
) }),
|
|
1712
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
|
|
1713
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Bold", toggle: true, active: s?.bold, disabled: off, onClick: () => run()?.toggleBold().run(), children: /* @__PURE__ */ jsx7("b", { children: "B" }) }),
|
|
1714
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Italic", toggle: true, active: s?.italic, disabled: off, onClick: () => run()?.toggleItalic().run(), children: /* @__PURE__ */ jsx7("i", { children: "I" }) }),
|
|
1715
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Underline", toggle: true, active: s?.underline, disabled: off, onClick: () => run()?.toggleUnderline().run(), children: /* @__PURE__ */ jsx7("u", { children: "U" }) }),
|
|
1716
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Strikethrough", toggle: true, active: s?.strike, disabled: off, onClick: () => run()?.toggleStrike().run(), children: /* @__PURE__ */ jsx7("s", { children: "S" }) })
|
|
1717
|
+
] }),
|
|
1718
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
|
|
1719
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Bullet list", toggle: true, active: s?.bullet, disabled: off, onClick: () => run()?.toggleBulletList().run(), children: "\u2022 List" }),
|
|
1720
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Numbered list", toggle: true, active: s?.ordered, disabled: off, onClick: () => run()?.toggleOrderedList().run(), children: "1. List" }),
|
|
1721
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Blockquote", toggle: true, active: s?.quote, disabled: off, onClick: () => run()?.toggleBlockquote().run(), children: "Quote" }),
|
|
1722
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Link", toggle: true, active: s?.link, disabled: off, onClick: openLink, children: "Link" })
|
|
1723
|
+
] }),
|
|
1724
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
|
|
1725
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Align left", toggle: true, active: s?.left, disabled: off, onClick: () => run()?.setTextAlign("left").run(), children: "L" }),
|
|
1726
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Align center", toggle: true, active: s?.center, disabled: off, onClick: () => run()?.setTextAlign("center").run(), children: "C" }),
|
|
1727
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Align right", toggle: true, active: s?.right, disabled: off, onClick: () => run()?.setTextAlign("right").run(), children: "R" }),
|
|
1728
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Justify", toggle: true, active: s?.justify, disabled: off, onClick: () => run()?.setTextAlign("justify").run(), children: "J" })
|
|
1729
|
+
] }),
|
|
1730
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group", children: [
|
|
1731
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Insert table", disabled: off, onClick: () => run()?.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: "Table" }),
|
|
1732
|
+
s?.inTable ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1733
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Add row below", disabled: off, onClick: () => run()?.addRowAfter().run(), children: "+Row" }),
|
|
1734
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Add column right", disabled: off, onClick: () => run()?.addColumnAfter().run(), children: "+Col" }),
|
|
1735
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Delete row", disabled: off, onClick: () => run()?.deleteRow().run(), children: "\u2212Row" }),
|
|
1736
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Delete column", disabled: off, onClick: () => run()?.deleteColumn().run(), children: "\u2212Col" }),
|
|
1737
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Delete table", disabled: off, onClick: () => run()?.deleteTable().run(), children: "Del table" })
|
|
1738
|
+
] }) : null
|
|
1739
|
+
] })
|
|
1740
|
+
] }) : null,
|
|
1741
|
+
/* @__PURE__ */ jsxs3("div", { className: "june-doc__group june-doc__group--end", children: [
|
|
1742
|
+
/* @__PURE__ */ jsx7(Btn, { label: "Copy document", onClick: onCopy, children: "Copy" }),
|
|
1743
|
+
exportFormats.length > 0 ? /* @__PURE__ */ jsxs3(
|
|
1744
|
+
"select",
|
|
1745
|
+
{
|
|
1746
|
+
className: "june-doc__select",
|
|
1747
|
+
"aria-label": "Export document",
|
|
1748
|
+
value: "",
|
|
1749
|
+
onChange: (e) => e.target.value && onExport(e.target.value),
|
|
1750
|
+
children: [
|
|
1751
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Export\u2026" }),
|
|
1752
|
+
exportFormats.map((f) => /* @__PURE__ */ jsx7("option", { value: f, children: FORMAT_LABELS[f] ?? f }, f))
|
|
1753
|
+
]
|
|
1754
|
+
}
|
|
1755
|
+
) : null,
|
|
1756
|
+
canSave ? /* @__PURE__ */ jsx7(Btn, { label: "Save", disabled: saveState === "saving" || saveState === "saved" || saveState === "idle", onClick: onSave, children: "Save" }) : null
|
|
1757
|
+
] }),
|
|
1758
|
+
linkOpen ? /* @__PURE__ */ jsxs3("div", { className: "june-doc__linkbar", children: [
|
|
1759
|
+
/* @__PURE__ */ jsx7(
|
|
1760
|
+
"input",
|
|
1761
|
+
{
|
|
1762
|
+
"aria-label": "Link address",
|
|
1763
|
+
className: "june-doc__input",
|
|
1764
|
+
placeholder: "https://example.com",
|
|
1765
|
+
value: linkValue,
|
|
1766
|
+
autoFocus: true,
|
|
1767
|
+
onChange: (e) => setLinkValue(e.target.value),
|
|
1768
|
+
onKeyDown: (e) => {
|
|
1769
|
+
if (e.key === "Enter") applyLink();
|
|
1770
|
+
if (e.key === "Escape") setLinkOpen(false);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
),
|
|
1774
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: applyLink, children: "Apply" }),
|
|
1775
|
+
s?.link ? /* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: removeLink, children: "Remove" }) : null,
|
|
1776
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "june-doc__btn", onClick: () => setLinkOpen(false), children: "Cancel" })
|
|
1777
|
+
] }) : null
|
|
1778
|
+
] });
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/components/document-editor/document-types.ts
|
|
1782
|
+
var ALL_EXPORT_FORMATS = [
|
|
1783
|
+
"html",
|
|
1784
|
+
"markdown",
|
|
1785
|
+
"txt",
|
|
1786
|
+
"docx",
|
|
1787
|
+
"pdf"
|
|
1788
|
+
];
|
|
1789
|
+
|
|
1790
|
+
// src/components/document-editor/document-editor.tsx
|
|
1791
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1792
|
+
var snapshotOf = (r) => ({
|
|
1793
|
+
documentId: r.id,
|
|
1794
|
+
title: r.title,
|
|
1795
|
+
documentType: r.documentType,
|
|
1796
|
+
status: r.status,
|
|
1797
|
+
json: r.json
|
|
1798
|
+
});
|
|
1799
|
+
var SAVE_LABEL = {
|
|
1800
|
+
idle: "",
|
|
1801
|
+
dirty: "Unsaved changes",
|
|
1802
|
+
saving: "Saving\u2026",
|
|
1803
|
+
saved: "Saved",
|
|
1804
|
+
error: "Save failed"
|
|
1805
|
+
};
|
|
1806
|
+
function DocumentEditorImpl(props) {
|
|
1807
|
+
const {
|
|
1808
|
+
documentId,
|
|
1809
|
+
title: titleProp = "Untitled document",
|
|
1810
|
+
content,
|
|
1811
|
+
documentType,
|
|
1812
|
+
status = "draft",
|
|
1813
|
+
editable = true,
|
|
1814
|
+
exportFormats,
|
|
1815
|
+
theme = "auto",
|
|
1816
|
+
className
|
|
1817
|
+
} = props;
|
|
1818
|
+
const ctxOptions = useDocumentOptions();
|
|
1819
|
+
const options = useMemo4(() => ({ ...ctxOptions, ...props.options }), [ctxOptions, props.options]);
|
|
1820
|
+
const optionsRef = useRef3(options);
|
|
1821
|
+
optionsRef.current = options;
|
|
1822
|
+
const store = useMemo4(() => options.store ?? getDefaultDocumentStore(), [options.store]);
|
|
1823
|
+
const instanceId = useId();
|
|
1824
|
+
const { theme: contextTheme } = useJuneTheme();
|
|
1825
|
+
const resolvedTheme = theme === "auto" ? contextTheme : theme;
|
|
1826
|
+
const contentKey = typeof content === "string" ? content : content ? JSON.stringify(content) : "";
|
|
1827
|
+
const parsed = useMemo4(() => {
|
|
1828
|
+
if (!contentKey) return { json: null, error: null };
|
|
1829
|
+
try {
|
|
1830
|
+
return { json: parseDocumentContent(contentKey), error: null };
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
return { json: null, error: toJuneError(e, "DOCUMENT_ERROR") };
|
|
1833
|
+
}
|
|
1834
|
+
}, [contentKey]);
|
|
1835
|
+
useEffect2(() => {
|
|
1836
|
+
if (parsed.error) optionsRef.current.onError?.(parsed.error);
|
|
1837
|
+
}, [parsed.error]);
|
|
1838
|
+
const title = useDocumentValue(store, documentId, (r) => r?.title ?? titleProp);
|
|
1839
|
+
const pending = useDocumentValue(store, documentId, (r) => r?.pending ?? null);
|
|
1840
|
+
const words = useDocumentValue(store, documentId, (r) => r ? wordCountOf(r.json) : 0);
|
|
1841
|
+
const recordStatus = useDocumentValue(store, documentId, (r) => r?.status ?? status);
|
|
1842
|
+
const [saveState, setSaveState] = useState3("idle");
|
|
1843
|
+
const [notice, setNotice] = useState3(null);
|
|
1844
|
+
const lastSavedKey = useRef3("");
|
|
1845
|
+
const seenRevision = useRef3(store.get(documentId)?.revision ?? 0);
|
|
1846
|
+
const readyRef = useRef3(false);
|
|
1847
|
+
const flash = useCallback3((message) => {
|
|
1848
|
+
setNotice(message);
|
|
1849
|
+
}, []);
|
|
1850
|
+
useEffect2(() => {
|
|
1851
|
+
if (!notice) return;
|
|
1852
|
+
const t = setTimeout(() => setNotice(null), 3e3);
|
|
1853
|
+
return () => clearTimeout(t);
|
|
1854
|
+
}, [notice]);
|
|
1855
|
+
const emitChange = useCallback3(
|
|
1856
|
+
(source) => {
|
|
1857
|
+
const record = store.get(documentId);
|
|
1858
|
+
if (record) optionsRef.current.onDocumentChange?.({ ...snapshotOf(record), source });
|
|
1859
|
+
},
|
|
1860
|
+
[store, documentId]
|
|
1861
|
+
);
|
|
1862
|
+
const saveRef = useRef3(async () => {
|
|
1863
|
+
});
|
|
1864
|
+
const autosave = useMemo4(() => {
|
|
1865
|
+
const delay = options.autosaveDelay ?? 1500;
|
|
1866
|
+
return delay > 0 ? debounce(() => void saveRef.current(), delay) : null;
|
|
1867
|
+
}, [options.autosaveDelay]);
|
|
1868
|
+
useEffect2(() => () => autosave?.flush(), [autosave]);
|
|
1869
|
+
saveRef.current = async () => {
|
|
1870
|
+
const record = store.get(documentId);
|
|
1871
|
+
const onSave = optionsRef.current.onSave;
|
|
1872
|
+
if (!record || !onSave) return;
|
|
1873
|
+
const key = JSON.stringify([record.title, docKey(record.json)]);
|
|
1874
|
+
if (key === lastSavedKey.current) {
|
|
1875
|
+
setSaveState("saved");
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
setSaveState("saving");
|
|
1879
|
+
try {
|
|
1880
|
+
await onSave({
|
|
1881
|
+
...snapshotOf(record),
|
|
1882
|
+
html: docToHtml(record.json),
|
|
1883
|
+
markdown: docToMarkdown(record.json)
|
|
1884
|
+
});
|
|
1885
|
+
lastSavedKey.current = key;
|
|
1886
|
+
const latest = store.get(documentId);
|
|
1887
|
+
const latestKey = latest ? JSON.stringify([latest.title, docKey(latest.json)]) : key;
|
|
1888
|
+
setSaveState(latestKey === key ? "saved" : "dirty");
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
setSaveState("error");
|
|
1891
|
+
optionsRef.current.onError?.(toJuneError(error, "DOCUMENT_ERROR", "Saving the document failed"));
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const markDirty = useCallback3(() => {
|
|
1895
|
+
if (!optionsRef.current.onSave) return;
|
|
1896
|
+
setSaveState("dirty");
|
|
1897
|
+
autosave?.();
|
|
1898
|
+
}, [autosave]);
|
|
1899
|
+
const [initialJson] = useState3(() => store.get(documentId)?.json ?? parsed.json ?? emptyDoc());
|
|
1900
|
+
const extensions = useMemo4(() => createDocumentExtensions(), []);
|
|
1901
|
+
const onUpdateRef = useRef3(() => {
|
|
1902
|
+
});
|
|
1903
|
+
onUpdateRef.current = (json) => {
|
|
1904
|
+
store.edit(documentId, json, instanceId);
|
|
1905
|
+
emitChange("user");
|
|
1906
|
+
markDirty();
|
|
1907
|
+
};
|
|
1908
|
+
const editor = useEditor({
|
|
1909
|
+
extensions,
|
|
1910
|
+
content: initialJson,
|
|
1911
|
+
editable,
|
|
1912
|
+
immediatelyRender: false,
|
|
1913
|
+
editorProps: {
|
|
1914
|
+
attributes: {
|
|
1915
|
+
class: "june-doc__prose",
|
|
1916
|
+
role: "textbox",
|
|
1917
|
+
"aria-multiline": "true",
|
|
1918
|
+
"aria-label": "Document content"
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
onUpdate: ({ editor: ed }) => onUpdateRef.current(ed.getJSON())
|
|
1922
|
+
});
|
|
1923
|
+
useEffect2(() => {
|
|
1924
|
+
editor?.setEditable(editable);
|
|
1925
|
+
}, [editor, editable]);
|
|
1926
|
+
useEffect2(() => {
|
|
1927
|
+
if (!parsed.json) return;
|
|
1928
|
+
const outcome = store.receive({
|
|
1929
|
+
id: documentId,
|
|
1930
|
+
title: titleProp,
|
|
1931
|
+
json: parsed.json,
|
|
1932
|
+
documentType,
|
|
1933
|
+
status
|
|
1934
|
+
});
|
|
1935
|
+
if (outcome === "applied" || outcome === "merged") {
|
|
1936
|
+
emitChange(outcome === "applied" ? "ai" : "merge");
|
|
1937
|
+
markDirty();
|
|
1938
|
+
} else if (outcome === "conflict") {
|
|
1939
|
+
const record = store.get(documentId);
|
|
1940
|
+
optionsRef.current.onConflict?.({
|
|
1941
|
+
documentId,
|
|
1942
|
+
title: record?.title ?? titleProp,
|
|
1943
|
+
conflicts: record?.pending?.conflicts ?? 1
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}, [store, documentId, titleProp, parsed.json, documentType, status, emitChange, markDirty]);
|
|
1947
|
+
useEffect2(() => {
|
|
1948
|
+
if (!editor) return;
|
|
1949
|
+
const apply = () => {
|
|
1950
|
+
const record = store.get(documentId);
|
|
1951
|
+
if (!record || record.revision === seenRevision.current) return;
|
|
1952
|
+
seenRevision.current = record.revision;
|
|
1953
|
+
if (record.origin === instanceId) return;
|
|
1954
|
+
const { from, to } = editor.state.selection;
|
|
1955
|
+
editor.commands.setContent(record.json, { emitUpdate: false });
|
|
1956
|
+
if (editor.isFocused) {
|
|
1957
|
+
const size = editor.state.doc.content.size;
|
|
1958
|
+
editor.commands.setTextSelection({ from: Math.min(from, size), to: Math.min(to, size) });
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
apply();
|
|
1962
|
+
return store.subscribe(apply);
|
|
1963
|
+
}, [editor, store, documentId, instanceId]);
|
|
1964
|
+
useEffect2(() => {
|
|
1965
|
+
if (!editor || readyRef.current) return;
|
|
1966
|
+
const record = store.get(documentId);
|
|
1967
|
+
if (!record) return;
|
|
1968
|
+
readyRef.current = true;
|
|
1969
|
+
lastSavedKey.current = JSON.stringify([record.title, docKey(record.json)]);
|
|
1970
|
+
optionsRef.current.onDocumentReady?.(snapshotOf(record));
|
|
1971
|
+
}, [editor, store, documentId, title]);
|
|
1972
|
+
const handleTitle = (value) => {
|
|
1973
|
+
store.setTitle(documentId, value);
|
|
1974
|
+
emitChange("user");
|
|
1975
|
+
markDirty();
|
|
1976
|
+
};
|
|
1977
|
+
const resolve = (choice) => {
|
|
1978
|
+
store.resolve(documentId, choice);
|
|
1979
|
+
if (choice !== "keep") {
|
|
1980
|
+
emitChange(choice === "merge" ? "merge" : "resolve");
|
|
1981
|
+
markDirty();
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
const handleExport = async (format) => {
|
|
1985
|
+
const record = store.get(documentId);
|
|
1986
|
+
if (!record) return;
|
|
1987
|
+
try {
|
|
1988
|
+
const result = await exportDocument(format, {
|
|
1989
|
+
documentId,
|
|
1990
|
+
title: record.title,
|
|
1991
|
+
json: record.json
|
|
1992
|
+
// always the latest editor content
|
|
1993
|
+
});
|
|
1994
|
+
if (result.blob) downloadResult(result);
|
|
1995
|
+
optionsRef.current.onExport?.({ documentId, format, ...result });
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
optionsRef.current.onError?.(toJuneError(error, "EXPORT_ERROR"));
|
|
1998
|
+
flash("Export failed");
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
const handleCopy = async () => {
|
|
2002
|
+
const record = store.get(documentId);
|
|
2003
|
+
if (!record) return;
|
|
2004
|
+
flash(await copyDocumentToClipboard({ title: record.title, json: record.json }) ? "Copied" : "Copy failed");
|
|
2005
|
+
};
|
|
2006
|
+
const formats = (exportFormats ?? [...ALL_EXPORT_FORMATS]).filter((f) => listDocumentExporters().includes(f));
|
|
2007
|
+
if (parsed.error && !store.get(documentId)) {
|
|
2008
|
+
return /* @__PURE__ */ jsxs4("div", { className: "june-doc june-doc--error", "data-theme": resolvedTheme, role: "alert", children: [
|
|
2009
|
+
/* @__PURE__ */ jsx8("p", { className: "june-doc__draft-text", children: "This document could not be displayed." }),
|
|
2010
|
+
/* @__PURE__ */ jsxs4("details", { children: [
|
|
2011
|
+
/* @__PURE__ */ jsx8("summary", { children: "Show raw content" }),
|
|
2012
|
+
/* @__PURE__ */ jsx8("pre", { className: "june-doc__raw", children: contentKey })
|
|
2013
|
+
] })
|
|
2014
|
+
] });
|
|
2015
|
+
}
|
|
2016
|
+
if (!editor) return /* @__PURE__ */ jsx8(DocumentDraft, { title, state: "loading" });
|
|
2017
|
+
const isDraft = recordStatus === "draft";
|
|
2018
|
+
return /* @__PURE__ */ jsxs4(
|
|
2019
|
+
"div",
|
|
2020
|
+
{
|
|
2021
|
+
className: className ? `june-doc ${className}` : "june-doc",
|
|
2022
|
+
"data-theme": resolvedTheme,
|
|
2023
|
+
"data-status": recordStatus,
|
|
2024
|
+
onKeyDown: (e) => {
|
|
2025
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s" && options.onSave) {
|
|
2026
|
+
e.preventDefault();
|
|
2027
|
+
void saveRef.current();
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
children: [
|
|
2031
|
+
/* @__PURE__ */ jsxs4("div", { className: "june-doc__header", children: [
|
|
2032
|
+
/* @__PURE__ */ jsx8(DocumentTitle, { value: title, disabled: !editable, onChange: handleTitle }),
|
|
2033
|
+
isDraft ? /* @__PURE__ */ jsx8("span", { className: "june-doc__badge", children: "AI draft" }) : null
|
|
2034
|
+
] }),
|
|
2035
|
+
pending ? /* @__PURE__ */ jsxs4("div", { className: "june-doc__banner", role: "status", children: [
|
|
2036
|
+
/* @__PURE__ */ jsxs4("span", { children: [
|
|
2037
|
+
"The assistant suggested changes that overlap your edits (",
|
|
2038
|
+
pending.conflicts,
|
|
2039
|
+
" ",
|
|
2040
|
+
pending.conflicts === 1 ? "section" : "sections",
|
|
2041
|
+
")."
|
|
2042
|
+
] }),
|
|
2043
|
+
/* @__PURE__ */ jsxs4("span", { className: "june-doc__banner-actions", children: [
|
|
2044
|
+
/* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("merge"), children: "Merge, keep my edits" }),
|
|
2045
|
+
/* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("replace"), children: "Use AI version" }),
|
|
2046
|
+
/* @__PURE__ */ jsx8("button", { type: "button", className: "june-doc__btn", onClick: () => resolve("keep"), children: "Keep mine" })
|
|
2047
|
+
] })
|
|
2048
|
+
] }) : null,
|
|
2049
|
+
/* @__PURE__ */ jsx8(
|
|
2050
|
+
DocumentToolbar,
|
|
2051
|
+
{
|
|
2052
|
+
editor,
|
|
2053
|
+
editable,
|
|
2054
|
+
exportFormats: formats,
|
|
2055
|
+
canSave: !!options.onSave,
|
|
2056
|
+
saveState,
|
|
2057
|
+
onExport: (f) => void handleExport(f),
|
|
2058
|
+
onCopy: () => void handleCopy(),
|
|
2059
|
+
onSave: () => void saveRef.current()
|
|
2060
|
+
}
|
|
2061
|
+
),
|
|
2062
|
+
/* @__PURE__ */ jsx8(DocumentContent, { editor }),
|
|
2063
|
+
/* @__PURE__ */ jsxs4("div", { className: "june-doc__footer", "aria-live": "polite", children: [
|
|
2064
|
+
/* @__PURE__ */ jsxs4("span", { children: [
|
|
2065
|
+
words,
|
|
2066
|
+
" words"
|
|
2067
|
+
] }),
|
|
2068
|
+
/* @__PURE__ */ jsx8("span", { children: notice ?? SAVE_LABEL[saveState] })
|
|
2069
|
+
] })
|
|
2070
|
+
]
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/components/document-editor/document-component.tsx
|
|
2076
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2077
|
+
var DOCUMENT_COMPONENT_DESCRIPTION = 'An editable document. DocumentEditor(documentId, title, content, documentType?, status?, editable?, exportFormats?). "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.';
|
|
2078
|
+
var documentProps = z.object({
|
|
2079
|
+
documentId: z.string().describe("Stable id. Reuse it when revising the same document."),
|
|
2080
|
+
title: z.string().describe("Document title. Do not repeat it as a heading in content."),
|
|
2081
|
+
content: z.string().describe("The whole document body as Markdown."),
|
|
2082
|
+
documentType: z.string().optional().describe("Kind of document, such as proposal, contract or invoice."),
|
|
2083
|
+
status: z.string().optional().describe('Use "draft" for generated documents.'),
|
|
2084
|
+
editable: z.boolean().optional().describe("Whether the user can edit. Defaults to true."),
|
|
2085
|
+
exportFormats: z.array(z.string()).optional().describe("Export formats to offer, such as html, markdown, docx.")
|
|
2086
|
+
});
|
|
2087
|
+
function DocumentEditor(props) {
|
|
2088
|
+
const { isStreaming } = useJuneRuntime();
|
|
2089
|
+
if (props.generating || isStreaming) {
|
|
2090
|
+
return /* @__PURE__ */ jsx9(DocumentDraft, { title: props.title, state: "drafting" });
|
|
2091
|
+
}
|
|
2092
|
+
return /* @__PURE__ */ jsx9(DocumentEditorImpl, { ...props });
|
|
2093
|
+
}
|
|
2094
|
+
function OpenUIDocument({ props }) {
|
|
2095
|
+
return /* @__PURE__ */ jsx9(
|
|
2096
|
+
DocumentEditor,
|
|
2097
|
+
{
|
|
2098
|
+
documentId: props.documentId,
|
|
2099
|
+
title: props.title,
|
|
2100
|
+
content: props.content,
|
|
2101
|
+
documentType: props.documentType,
|
|
2102
|
+
status: props.status,
|
|
2103
|
+
editable: props.editable,
|
|
2104
|
+
exportFormats: props.exportFormats
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
var JuneDocumentEditorComponent = defineComponent2({
|
|
2109
|
+
name: "DocumentEditor",
|
|
2110
|
+
description: DOCUMENT_COMPONENT_DESCRIPTION,
|
|
2111
|
+
props: documentProps,
|
|
2112
|
+
component: OpenUIDocument
|
|
2113
|
+
});
|
|
2114
|
+
function withDocumentEditor(library) {
|
|
2115
|
+
return library.extend({ components: { add: [JuneDocumentEditorComponent] } });
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// src/components/document-editor/document-instructions.ts
|
|
2119
|
+
var DEFAULT_TYPES = [
|
|
2120
|
+
"proposal",
|
|
2121
|
+
"report",
|
|
2122
|
+
"email",
|
|
2123
|
+
"letter",
|
|
2124
|
+
"invoice",
|
|
2125
|
+
"contract",
|
|
2126
|
+
"NDA",
|
|
2127
|
+
"memo",
|
|
2128
|
+
"meeting notes"
|
|
2129
|
+
];
|
|
2130
|
+
function getDocumentEditorRules(options = {}) {
|
|
2131
|
+
const name = options.componentName ?? "DocumentEditor";
|
|
2132
|
+
const types = (options.documentTypes ?? DEFAULT_TYPES).join(", ");
|
|
2133
|
+
return [
|
|
2134
|
+
`WHEN TO USE ${name}: use it only when the user asks you to create, draft, write, prepare, generate or revise an editable document (${types}, or similar). Put the finished document inside ${name}.`,
|
|
2135
|
+
`WHEN NOT TO USE ${name}: for explanations ("explain what an NDA is"), factual answers, comparisons, brainstorming and general questions, answer with normal text and the other components. Never open ${name} for those.`,
|
|
2136
|
+
`${name}(documentId, title, content, documentType?, status?, editable?, exportFormats?): "content" is the whole document body as Markdown inside ONE double-quoted string. Write line breaks as \\n and escape double quotes as \\". Supported Markdown: headings (#, ##, ###), paragraphs, **bold**, *italic*, lists, > quotes, [links](https://...), and tables.`,
|
|
2137
|
+
`Do not repeat the title as a heading inside "content"; the title is shown separately. Structure the document with headings, short paragraphs and tables where useful. Keep a professional tone.`,
|
|
2138
|
+
`EDITING: when the user asks to change an existing document (add a section, rewrite a part, change the title), write ${name} again with the SAME documentId and the COMPLETE updated content, starting from the current document text you were given. Copy every unchanged section exactly as it was. Change only what was requested. The app merges your version with the user's own edits.`,
|
|
2139
|
+
`If the user asks for a document but leaves out details, write the document anyway and use clear placeholders such as [CLIENT NAME], [DATE], [AMOUNT]. Do not stop to ask unless the request is impossible to interpret.`,
|
|
2140
|
+
`Never invent legal, financial or business facts, prices, dates, names or figures. Use placeholders instead. Set status to "draft" for generated documents, and for legal or financial documents say in one short sentence outside the component that the document is a draft that should be reviewed by a professional.`,
|
|
2141
|
+
`You may add one short sentence before or after the component. Do not repeat the document text outside the component.`,
|
|
2142
|
+
...options.extraRules ?? []
|
|
2143
|
+
];
|
|
2144
|
+
}
|
|
2145
|
+
function getDocumentEditorInstructions(options = {}) {
|
|
2146
|
+
const rules = getDocumentEditorRules(options);
|
|
2147
|
+
return ["Document drafting:", ...rules.map((rule, i) => `${i + 1}. ${rule}`)].join("\n");
|
|
2148
|
+
}
|
|
2149
|
+
var EXAMPLE_NEW = 'root = Stack([doc])\ndoc = DocumentEditor("business-proposal", "Business Proposal", "## Executive Summary\\n\\nThis proposal outlines [SERVICE] for [CLIENT NAME].\\n\\n## Scope of Work\\n\\n- Discovery\\n- Delivery\\n\\n## Pricing\\n\\n| Item | Cost |\\n| --- | --- |\\n| Discovery | [AMOUNT] |", "proposal", "draft")';
|
|
2150
|
+
var EXAMPLE_EDIT = 'root = Stack([doc])\ndoc = DocumentEditor("business-proposal", "Business Proposal", "## Executive Summary\\n\\nThis proposal outlines [SERVICE] for [CLIENT NAME].\\n\\n## Scope of Work\\n\\n- Discovery\\n- Delivery\\n\\n## Pricing\\n\\n| Item | Cost |\\n| --- | --- |\\n| Discovery | [AMOUNT] |\\n| Delivery | [AMOUNT] |", "proposal", "draft")';
|
|
2151
|
+
function documentPromptOptions(options = {}) {
|
|
2152
|
+
return {
|
|
2153
|
+
additionalRules: getDocumentEditorRules(options),
|
|
2154
|
+
examples: [EXAMPLE_NEW, EXAMPLE_EDIT]
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/components/document-editor/document-agent.ts
|
|
2159
|
+
var attr = (value) => value.replace(/"/g, """);
|
|
2160
|
+
async function serializeDocumentsForAgent(options = {}) {
|
|
2161
|
+
const store = options.store ?? getDefaultDocumentStore();
|
|
2162
|
+
const { docToMarkdown: docToMarkdown2 } = await import("./document-engine-C6ILFMRP.js");
|
|
2163
|
+
return store.list().filter((r) => !options.ids || options.ids.includes(r.id)).map(
|
|
2164
|
+
(r) => `<document id="${attr(r.id)}" title="${attr(r.title)}"${r.documentType ? ` type="${attr(r.documentType)}"` : ""}>
|
|
2165
|
+
${docToMarkdown2(r.json)}
|
|
2166
|
+
</document>`
|
|
2167
|
+
).join("\n\n");
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/components/june-library.ts
|
|
1056
2171
|
import { openuiLibrary } from "@openuidev/react-ui";
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
2172
|
+
var juneLibrary = withDocumentEditor(openuiLibrary);
|
|
2173
|
+
|
|
2174
|
+
// src/core/JuneUI.tsx
|
|
2175
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
2176
|
+
function JuneUI({ library = juneLibrary, ...props }) {
|
|
2177
|
+
return /* @__PURE__ */ jsx10(JuneRenderer, { library, ...props });
|
|
1060
2178
|
}
|
|
1061
2179
|
export {
|
|
2180
|
+
ALL_EXPORT_FORMATS,
|
|
2181
|
+
DocumentEditor,
|
|
2182
|
+
JuneDocumentContext,
|
|
2183
|
+
JuneDocumentEditorComponent,
|
|
1062
2184
|
JuneError,
|
|
1063
2185
|
JuneErrorBoundary,
|
|
1064
2186
|
JuneRenderer,
|
|
2187
|
+
JuneRuntimeContext,
|
|
1065
2188
|
JuneThemeProvider,
|
|
1066
2189
|
JuneUI,
|
|
1067
2190
|
consumeStream,
|
|
2191
|
+
copyDocumentToClipboard,
|
|
1068
2192
|
createActionHandler,
|
|
2193
|
+
createDocumentStore,
|
|
1069
2194
|
createJuneLibrary,
|
|
1070
2195
|
darkTheme,
|
|
1071
2196
|
defaultParseActionEvent,
|
|
1072
2197
|
defineJuneAction,
|
|
1073
2198
|
defineJuneComponent,
|
|
2199
|
+
documentPromptOptions,
|
|
2200
|
+
downloadResult,
|
|
2201
|
+
exportDocument,
|
|
1074
2202
|
fetchStream,
|
|
2203
|
+
getDefaultDocumentStore,
|
|
2204
|
+
getDocumentEditorInstructions,
|
|
2205
|
+
getDocumentEditorRules,
|
|
1075
2206
|
isJuneError,
|
|
1076
2207
|
jsonFieldExtractor,
|
|
2208
|
+
juneLibrary,
|
|
1077
2209
|
lightTheme,
|
|
2210
|
+
listDocumentExporters,
|
|
2211
|
+
mergeDocuments,
|
|
2212
|
+
registerDocumentExporter,
|
|
1078
2213
|
runAction,
|
|
2214
|
+
serializeDocumentsForAgent,
|
|
1079
2215
|
sseToText,
|
|
1080
2216
|
toJuneError,
|
|
2217
|
+
useDocumentOptions,
|
|
2218
|
+
useJuneRuntime,
|
|
1081
2219
|
useJuneStream,
|
|
1082
2220
|
useJuneTheme,
|
|
1083
|
-
useResolvedTheme
|
|
2221
|
+
useResolvedTheme,
|
|
2222
|
+
withDocumentEditor
|
|
1084
2223
|
};
|