@june-ai/june-ui 0.2.4 → 0.2.5
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/CHANGELOG.md +21 -0
- package/dist/chunk-CZE533PM.js +343 -0
- package/dist/{document-engine-C6ILFMRP.js → document-engine-IAY2T6B3.js} +1 -1
- package/dist/index.cjs +1626 -291
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1428 -258
- package/dist/june.css +436 -118
- package/package.json +6 -1
- package/dist/chunk-HFHRSNIL.js +0 -174
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased] — DocumentEditor premium UI
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Document app bar: editable title, AI-draft badge, Save, **Download** menu (Word, PDF, HTML,
|
|
7
|
+
Markdown, plain text, copy to clipboard) and full-screen mode (Esc / ✕ to close).
|
|
8
|
+
- Grouped ribbon with labels and More/Less: History · Format (bold, italic, underline, strike,
|
|
9
|
+
inline code, superscript, subscript) · Align · Lists (with indent/outdent) / Style (paragraph,
|
|
10
|
+
headings, quote, code block) · Font family · Font size · formatting marks (¶) · Document (page
|
|
11
|
+
break, divider, page/full-width layout, find & replace) · Color (text color, highlight) ·
|
|
12
|
+
Insert (link, image upload/URL, table grid picker) · Actions (clear formatting).
|
|
13
|
+
- Contextual table tools (rows, columns, merge/split, header row, delete table).
|
|
14
|
+
- Selection bubble menu, A4 page canvas with page boundaries, "Page X of Y", word count, zoom.
|
|
15
|
+
- Find & replace (`Ctrl/Cmd+F`) with match highlighting, match case, replace and replace all.
|
|
16
|
+
- Page break node (`Ctrl/Cmd+Enter`); DOCX/PDF/HTML exports start a new page there.
|
|
17
|
+
- DOCX export now keeps text color, highlight, font family, font size, super/subscript and images.
|
|
18
|
+
- New dependencies: `@tiptap/extension-text-style`, `-highlight`, `-image`, `-subscript`, `-superscript`.
|
|
19
|
+
|
|
20
|
+
### Compatibility
|
|
21
|
+
- Props, callbacks (`onSave`, `onExport`, `onDocumentChange`, `onConflict`, …), the store, AI merge,
|
|
22
|
+
autosave, `Ctrl/Cmd+S` and the OpenUI `DocumentEditor(...)` signature are unchanged.
|
|
23
|
+
|
|
3
24
|
## [Unreleased] (planned 0.2.0)
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// src/components/document-editor/document-engine.ts
|
|
2
|
+
import { Editor } from "@tiptap/core";
|
|
3
|
+
|
|
4
|
+
// src/errors/errors.ts
|
|
5
|
+
var JuneError = class _JuneError extends Error {
|
|
6
|
+
constructor(code, message, options = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
Object.setPrototypeOf(this, _JuneError.prototype);
|
|
9
|
+
this.name = "JuneError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = options.details;
|
|
12
|
+
if (options.cause !== void 0) {
|
|
13
|
+
this.cause = options.cause;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function isJuneError(value) {
|
|
18
|
+
return value instanceof JuneError;
|
|
19
|
+
}
|
|
20
|
+
function toJuneError(error, fallbackCode, message) {
|
|
21
|
+
if (error instanceof JuneError) return error;
|
|
22
|
+
const text = message ?? (error instanceof Error ? error.message : String(error));
|
|
23
|
+
return new JuneError(fallbackCode, text, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/components/document-editor/document-extensions.ts
|
|
27
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
28
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
29
|
+
import Image from "@tiptap/extension-image";
|
|
30
|
+
import Subscript from "@tiptap/extension-subscript";
|
|
31
|
+
import Superscript from "@tiptap/extension-superscript";
|
|
32
|
+
import { TableKit } from "@tiptap/extension-table";
|
|
33
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
34
|
+
import { Color, FontFamily, FontSize, TextStyle } from "@tiptap/extension-text-style";
|
|
35
|
+
import { Markdown } from "@tiptap/markdown";
|
|
36
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
37
|
+
|
|
38
|
+
// src/components/document-editor/document-search.ts
|
|
39
|
+
import { Extension } from "@tiptap/core";
|
|
40
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
41
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
42
|
+
var searchKey = new PluginKey("juneDocSearch");
|
|
43
|
+
function findMatches(doc, term, caseSensitive) {
|
|
44
|
+
if (!term) return [];
|
|
45
|
+
const needle = caseSensitive ? term : term.toLowerCase();
|
|
46
|
+
const out = [];
|
|
47
|
+
doc.descendants((node, pos) => {
|
|
48
|
+
if (!node.isTextblock) return true;
|
|
49
|
+
let text = "";
|
|
50
|
+
const map = [];
|
|
51
|
+
node.forEach((child, offset) => {
|
|
52
|
+
const start = pos + 1 + offset;
|
|
53
|
+
if (child.isText && child.text) {
|
|
54
|
+
for (let i = 0; i < child.text.length; i++) map.push(start + i);
|
|
55
|
+
text += child.text;
|
|
56
|
+
} else {
|
|
57
|
+
map.push(start);
|
|
58
|
+
text += "\uFFFC";
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const hay = caseSensitive ? text : text.toLowerCase();
|
|
62
|
+
let idx = hay.indexOf(needle);
|
|
63
|
+
while (idx !== -1) {
|
|
64
|
+
out.push({ from: map[idx], to: map[idx + needle.length - 1] + 1 });
|
|
65
|
+
idx = hay.indexOf(needle, idx + needle.length);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
});
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function build(doc, term, caseSensitive, current) {
|
|
72
|
+
const matches = findMatches(doc, term, caseSensitive);
|
|
73
|
+
const active = matches.length ? Math.min(Math.max(current, 0), matches.length - 1) : -1;
|
|
74
|
+
const decorations = DecorationSet.create(
|
|
75
|
+
doc,
|
|
76
|
+
matches.map(
|
|
77
|
+
(m, i) => Decoration.inline(m.from, m.to, {
|
|
78
|
+
class: i === active ? "june-doc__match june-doc__match--active" : "june-doc__match"
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
return { term, caseSensitive, current: active, matches, decorations };
|
|
83
|
+
}
|
|
84
|
+
var DocumentSearch = Extension.create({
|
|
85
|
+
name: "juneDocSearch",
|
|
86
|
+
addProseMirrorPlugins() {
|
|
87
|
+
return [
|
|
88
|
+
new Plugin({
|
|
89
|
+
key: searchKey,
|
|
90
|
+
state: {
|
|
91
|
+
init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations: DecorationSet.empty }),
|
|
92
|
+
apply(tr, prev, _old, state) {
|
|
93
|
+
const meta = tr.getMeta(searchKey);
|
|
94
|
+
if (!meta && !tr.docChanged) return prev;
|
|
95
|
+
if (!meta && !prev.term) return prev;
|
|
96
|
+
return build(
|
|
97
|
+
state.doc,
|
|
98
|
+
meta?.term ?? prev.term,
|
|
99
|
+
meta?.caseSensitive ?? prev.caseSensitive,
|
|
100
|
+
meta?.current ?? prev.current
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
props: {
|
|
105
|
+
decorations: (state) => searchKey.getState(state)?.decorations
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
function getSearchState(editor) {
|
|
112
|
+
return editor ? searchKey.getState(editor.state) : void 0;
|
|
113
|
+
}
|
|
114
|
+
function setSearch(editor, meta) {
|
|
115
|
+
editor.view.dispatch(editor.state.tr.setMeta(searchKey, meta));
|
|
116
|
+
}
|
|
117
|
+
function gotoMatch(editor, step) {
|
|
118
|
+
const s = getSearchState(editor);
|
|
119
|
+
if (!s || !s.matches.length) return;
|
|
120
|
+
const n = s.matches.length;
|
|
121
|
+
const current = s.current < 0 ? 0 : (s.current + step + n) % n;
|
|
122
|
+
setSearch(editor, { current });
|
|
123
|
+
const m = s.matches[current];
|
|
124
|
+
editor.chain().setTextSelection(m).scrollIntoView().run();
|
|
125
|
+
}
|
|
126
|
+
function replaceCurrent(editor, replacement) {
|
|
127
|
+
const s = getSearchState(editor);
|
|
128
|
+
if (!s || s.current < 0) return;
|
|
129
|
+
const m = s.matches[s.current];
|
|
130
|
+
editor.chain().command(({ tr }) => {
|
|
131
|
+
if (replacement) tr.insertText(replacement, m.from, m.to);
|
|
132
|
+
else tr.delete(m.from, m.to);
|
|
133
|
+
return true;
|
|
134
|
+
}).run();
|
|
135
|
+
gotoMatch(editor, 0);
|
|
136
|
+
}
|
|
137
|
+
function replaceAll(editor, replacement) {
|
|
138
|
+
const s = getSearchState(editor);
|
|
139
|
+
if (!s || !s.matches.length) return 0;
|
|
140
|
+
const matches = [...s.matches].reverse();
|
|
141
|
+
editor.chain().command(({ tr }) => {
|
|
142
|
+
for (const m of matches) {
|
|
143
|
+
if (replacement) tr.insertText(replacement, m.from, m.to);
|
|
144
|
+
else tr.delete(m.from, m.to);
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}).run();
|
|
148
|
+
return matches.length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/components/document-editor/document-extensions.ts
|
|
152
|
+
var PageBreak = Node.create({
|
|
153
|
+
name: "pageBreak",
|
|
154
|
+
group: "block",
|
|
155
|
+
atom: true,
|
|
156
|
+
selectable: true,
|
|
157
|
+
parseHTML() {
|
|
158
|
+
return [{ tag: "div[data-page-break]" }];
|
|
159
|
+
},
|
|
160
|
+
renderHTML({ HTMLAttributes }) {
|
|
161
|
+
return [
|
|
162
|
+
"div",
|
|
163
|
+
mergeAttributes(HTMLAttributes, {
|
|
164
|
+
"data-page-break": "true",
|
|
165
|
+
class: "june-doc__page-break",
|
|
166
|
+
style: "break-after: page; page-break-after: always;"
|
|
167
|
+
})
|
|
168
|
+
];
|
|
169
|
+
},
|
|
170
|
+
// Markdown has no page break; a thematic break is the closest stable equivalent.
|
|
171
|
+
renderMarkdown: () => "---",
|
|
172
|
+
addCommands() {
|
|
173
|
+
return {
|
|
174
|
+
setPageBreak: () => ({ chain }) => chain().insertContent([{ type: this.name }, { type: "paragraph" }]).run()
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
addKeyboardShortcuts() {
|
|
178
|
+
return { "Mod-Enter": () => this.editor.commands.setPageBreak() };
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
function createDocumentExtensions() {
|
|
182
|
+
return [
|
|
183
|
+
StarterKit.configure({
|
|
184
|
+
heading: { levels: [1, 2, 3] },
|
|
185
|
+
// Tiptap v3 StarterKit already includes Link and Underline
|
|
186
|
+
link: { openOnClick: false, autolink: true, defaultProtocol: "https" }
|
|
187
|
+
}),
|
|
188
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
189
|
+
TableKit,
|
|
190
|
+
TextStyle,
|
|
191
|
+
Color,
|
|
192
|
+
FontFamily,
|
|
193
|
+
FontSize,
|
|
194
|
+
Highlight.configure({ multicolor: true }),
|
|
195
|
+
Subscript,
|
|
196
|
+
Superscript,
|
|
197
|
+
Image.configure({
|
|
198
|
+
inline: false,
|
|
199
|
+
allowBase64: true,
|
|
200
|
+
resize: { enabled: true, minWidth: 48, minHeight: 32, alwaysPreserveAspectRatio: true }
|
|
201
|
+
}),
|
|
202
|
+
PageBreak,
|
|
203
|
+
DocumentSearch,
|
|
204
|
+
Markdown
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/components/document-editor/document-utils.ts
|
|
209
|
+
var emptyDoc = () => ({ type: "doc", content: [{ type: "paragraph" }] });
|
|
210
|
+
function isDocJSON(value) {
|
|
211
|
+
if (!value || typeof value !== "object") return false;
|
|
212
|
+
const v = value;
|
|
213
|
+
return v.type === "doc" && (v.content === void 0 || Array.isArray(v.content));
|
|
214
|
+
}
|
|
215
|
+
function looksLikeDocJSON(text) {
|
|
216
|
+
return /^\s*\{\s*"type"\s*:\s*"doc"/.test(text);
|
|
217
|
+
}
|
|
218
|
+
function tryParseDocJSON(text) {
|
|
219
|
+
try {
|
|
220
|
+
const value = JSON.parse(text);
|
|
221
|
+
return isDocJSON(value) ? value : null;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var keyCache = /* @__PURE__ */ new WeakMap();
|
|
227
|
+
function docKey(doc) {
|
|
228
|
+
let key = keyCache.get(doc);
|
|
229
|
+
if (key === void 0) {
|
|
230
|
+
key = JSON.stringify(doc);
|
|
231
|
+
keyCache.set(doc, key);
|
|
232
|
+
}
|
|
233
|
+
return key;
|
|
234
|
+
}
|
|
235
|
+
function plainText(node) {
|
|
236
|
+
const n = node;
|
|
237
|
+
if (typeof n.text === "string") return n.text;
|
|
238
|
+
return (n.content ?? []).map(plainText).join(" ");
|
|
239
|
+
}
|
|
240
|
+
function countWords(text) {
|
|
241
|
+
const trimmed = text.trim();
|
|
242
|
+
return trimmed ? trimmed.split(/\s+/).length : 0;
|
|
243
|
+
}
|
|
244
|
+
var wordCache = /* @__PURE__ */ new WeakMap();
|
|
245
|
+
function wordCountOf(doc) {
|
|
246
|
+
let count = wordCache.get(doc);
|
|
247
|
+
if (count === void 0) {
|
|
248
|
+
count = countWords(plainText(doc));
|
|
249
|
+
wordCache.set(doc, count);
|
|
250
|
+
}
|
|
251
|
+
return count;
|
|
252
|
+
}
|
|
253
|
+
function slugify(value) {
|
|
254
|
+
return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
255
|
+
}
|
|
256
|
+
function debounce(fn, wait) {
|
|
257
|
+
let timer = null;
|
|
258
|
+
const run = () => {
|
|
259
|
+
timer = null;
|
|
260
|
+
fn();
|
|
261
|
+
};
|
|
262
|
+
const debounced = (() => {
|
|
263
|
+
if (timer !== null) clearTimeout(timer);
|
|
264
|
+
timer = setTimeout(run, wait);
|
|
265
|
+
});
|
|
266
|
+
debounced.cancel = () => {
|
|
267
|
+
if (timer !== null) {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
timer = null;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
debounced.flush = () => {
|
|
273
|
+
if (timer !== null) {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
run();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return debounced;
|
|
279
|
+
}
|
|
280
|
+
function sanitizeHref(input) {
|
|
281
|
+
const value = input.trim();
|
|
282
|
+
if (!value) return null;
|
|
283
|
+
if (/^(https?:|mailto:|tel:)/i.test(value)) return value;
|
|
284
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;
|
|
285
|
+
if (value.startsWith("/") || value.startsWith("#")) return value;
|
|
286
|
+
return `https://${value}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/components/document-editor/document-engine.ts
|
|
290
|
+
var scratch = null;
|
|
291
|
+
function scratchEditor() {
|
|
292
|
+
if (!scratch || scratch.isDestroyed) {
|
|
293
|
+
scratch = new Editor({ extensions: createDocumentExtensions(), content: "" });
|
|
294
|
+
}
|
|
295
|
+
return scratch;
|
|
296
|
+
}
|
|
297
|
+
function parseDocumentContent(text) {
|
|
298
|
+
const editor = scratchEditor();
|
|
299
|
+
const trimmed = text.trim();
|
|
300
|
+
try {
|
|
301
|
+
if (looksLikeDocJSON(trimmed)) {
|
|
302
|
+
const json = tryParseDocJSON(trimmed);
|
|
303
|
+
if (!json) throw new Error("The document JSON could not be parsed");
|
|
304
|
+
editor.schema.nodeFromJSON(json).check();
|
|
305
|
+
editor.commands.setContent(json, { emitUpdate: false });
|
|
306
|
+
} else {
|
|
307
|
+
editor.commands.setContent(trimmed, { contentType: "markdown", emitUpdate: false });
|
|
308
|
+
}
|
|
309
|
+
return editor.getJSON();
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw toJuneError(error, "DOCUMENT_ERROR", "The document content is invalid");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function load(doc) {
|
|
315
|
+
const editor = scratchEditor();
|
|
316
|
+
editor.commands.setContent(doc, { emitUpdate: false });
|
|
317
|
+
return editor;
|
|
318
|
+
}
|
|
319
|
+
var docToHtml = (doc) => load(doc).getHTML();
|
|
320
|
+
var docToMarkdown = (doc) => load(doc).getMarkdown();
|
|
321
|
+
var docToText = (doc) => load(doc).getText({ blockSeparator: "\n\n" });
|
|
322
|
+
|
|
323
|
+
export {
|
|
324
|
+
JuneError,
|
|
325
|
+
isJuneError,
|
|
326
|
+
toJuneError,
|
|
327
|
+
emptyDoc,
|
|
328
|
+
docKey,
|
|
329
|
+
wordCountOf,
|
|
330
|
+
slugify,
|
|
331
|
+
debounce,
|
|
332
|
+
sanitizeHref,
|
|
333
|
+
getSearchState,
|
|
334
|
+
setSearch,
|
|
335
|
+
gotoMatch,
|
|
336
|
+
replaceCurrent,
|
|
337
|
+
replaceAll,
|
|
338
|
+
createDocumentExtensions,
|
|
339
|
+
parseDocumentContent,
|
|
340
|
+
docToHtml,
|
|
341
|
+
docToMarkdown,
|
|
342
|
+
docToText
|
|
343
|
+
};
|