@june-ai/june-ui 0.2.5 → 0.2.7
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 +7 -0
- package/dist/block-automount.js +2 -0
- package/dist/{chunk-CZE533PM.js → chunk-VFZI3SEL.js} +186 -9
- package/dist/{document-engine-IAY2T6B3.js → document-engine-RKD5SLXR.js} +1 -1
- package/dist/index.cjs +1134 -679
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +889 -615
- package/dist/june.css +43 -19
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
- Page break node (`Ctrl/Cmd+Enter`); DOCX/PDF/HTML exports start a new page there.
|
|
17
17
|
- DOCX export now keeps text color, highlight, font family, font size, super/subscript and images.
|
|
18
18
|
- New dependencies: `@tiptap/extension-text-style`, `-highlight`, `-image`, `-subscript`, `-superscript`.
|
|
19
|
+
- Real pagination: blocks that do not fit move to the next A4 sheet, with a visible gap and page numbers.
|
|
20
|
+
- `JuneUI`/`JuneRenderer` `onResponseChange(response, info)` (+ `responseChangeDelay`): user edits to a
|
|
21
|
+
document (body as Markdown, and title) are written back into the OpenUI program. The rewritten
|
|
22
|
+
program can be passed back as `response`; it is recognised as an echo, not a new AI version.
|
|
23
|
+
- `updateDocumentInProgram`, `readDocumentFromProgram`, `rememberDocumentEcho`, `isDocumentEcho`.
|
|
24
|
+
- Save appears next to Download as soon as the user edits, even without `onSave`.
|
|
25
|
+
- Toolbar layout adapts to the editor's own width (container queries) instead of the window.
|
|
19
26
|
|
|
20
27
|
### Compatibility
|
|
21
28
|
- Props, callbacks (`onSave`, `onExport`, `onDocumentChange`, `onConflict`, …), the store, AI merge,
|
|
@@ -35,11 +35,183 @@ import { Color, FontFamily, FontSize, TextStyle } from "@tiptap/extension-text-s
|
|
|
35
35
|
import { Markdown } from "@tiptap/markdown";
|
|
36
36
|
import StarterKit from "@tiptap/starter-kit";
|
|
37
37
|
|
|
38
|
-
// src/components/document-editor/document-
|
|
38
|
+
// src/components/document-editor/document-pagination.ts
|
|
39
39
|
import { Extension } from "@tiptap/core";
|
|
40
40
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
41
41
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
42
|
-
var
|
|
42
|
+
var paginationKey = new PluginKey("juneDocPagination");
|
|
43
|
+
var configs = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
var SPLITTABLE = /* @__PURE__ */ new Set(["bulletList", "orderedList", "listItem", "blockquote"]);
|
|
45
|
+
function gapWidget(height) {
|
|
46
|
+
return () => {
|
|
47
|
+
const el = document.createElement("div");
|
|
48
|
+
el.className = "june-doc__page-gap";
|
|
49
|
+
el.contentEditable = "false";
|
|
50
|
+
el.setAttribute("aria-hidden", "true");
|
|
51
|
+
el.style.height = `${height}px`;
|
|
52
|
+
return el;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function build(doc, breaks) {
|
|
56
|
+
return DecorationSet.create(
|
|
57
|
+
doc,
|
|
58
|
+
breaks.map(
|
|
59
|
+
(b) => Decoration.widget(b.pos, gapWidget(b.height), {
|
|
60
|
+
side: -1,
|
|
61
|
+
ignoreSelection: true,
|
|
62
|
+
key: `jd-gap-${b.pos}-${Math.round(b.height)}`
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function collectUnits(doc) {
|
|
68
|
+
const units = [];
|
|
69
|
+
const walk = (parent, start, inheritedBreak) => {
|
|
70
|
+
parent.forEach((child, offset, index) => {
|
|
71
|
+
const pos = start + offset;
|
|
72
|
+
const breakPos = index === 0 && inheritedBreak !== null ? inheritedBreak : pos;
|
|
73
|
+
if (SPLITTABLE.has(child.type.name) && child.childCount > 0) {
|
|
74
|
+
walk(child, pos + 1, breakPos);
|
|
75
|
+
} else {
|
|
76
|
+
units.push({ pos, breakPos, node: child });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
walk(doc, 0, null);
|
|
81
|
+
return units;
|
|
82
|
+
}
|
|
83
|
+
function measure(view, cfg) {
|
|
84
|
+
const prose = view.dom;
|
|
85
|
+
const proseRect = prose.getBoundingClientRect();
|
|
86
|
+
const scale = prose.offsetWidth ? proseRect.width / prose.offsetWidth : 1;
|
|
87
|
+
if (!scale || !proseRect.width) return { breaks: [], pages: 1 };
|
|
88
|
+
const gaps = [...prose.querySelectorAll(".june-doc__page-gap")].map((g) => {
|
|
89
|
+
const r = g.getBoundingClientRect();
|
|
90
|
+
return { top: (r.top - proseRect.top) / scale, height: r.height / scale };
|
|
91
|
+
});
|
|
92
|
+
const gapsAbove = (y) => gaps.reduce((sum, g) => g.top <= y + 0.5 ? sum + g.height : sum, 0);
|
|
93
|
+
const contentH = Math.max(80, cfg.pageHeight - cfg.padTop - cfg.padBottom);
|
|
94
|
+
const stride = cfg.pageHeight + cfg.gap;
|
|
95
|
+
const breaks = /* @__PURE__ */ new Map();
|
|
96
|
+
let pageStart = 0;
|
|
97
|
+
let added = 0;
|
|
98
|
+
let force = false;
|
|
99
|
+
for (const unit of collectUnits(view.state.doc)) {
|
|
100
|
+
const el = view.nodeDOM(unit.pos);
|
|
101
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
102
|
+
const r = el.getBoundingClientRect();
|
|
103
|
+
const rawTop = (r.top - proseRect.top) / scale;
|
|
104
|
+
const natural = rawTop - gapsAbove(rawTop);
|
|
105
|
+
const h = r.height / scale;
|
|
106
|
+
let top = natural + added;
|
|
107
|
+
let bottom = top + h;
|
|
108
|
+
const overflows = bottom > pageStart + contentH + 0.5 && top > pageStart + 1;
|
|
109
|
+
if (force || overflows) {
|
|
110
|
+
const target = pageStart + stride;
|
|
111
|
+
if (top < target) {
|
|
112
|
+
const spacer = target - top;
|
|
113
|
+
breaks.set(unit.breakPos, (breaks.get(unit.breakPos) ?? 0) + spacer);
|
|
114
|
+
added += spacer;
|
|
115
|
+
top = target;
|
|
116
|
+
bottom = top + h;
|
|
117
|
+
}
|
|
118
|
+
pageStart = target;
|
|
119
|
+
while (top >= pageStart + stride) pageStart += stride;
|
|
120
|
+
force = false;
|
|
121
|
+
}
|
|
122
|
+
while (bottom > pageStart + contentH + 0.5) pageStart += stride;
|
|
123
|
+
if (unit.node.type.name === "pageBreak") force = true;
|
|
124
|
+
}
|
|
125
|
+
const pages = Math.max(1, Math.round(pageStart / stride) + 1 + (force ? 1 : 0));
|
|
126
|
+
return {
|
|
127
|
+
breaks: [...breaks].map(([pos, height]) => ({ pos, height: Math.round(height * 10) / 10 })).sort((a, b) => a.pos - b.pos),
|
|
128
|
+
pages
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
var same = (a, b) => a.length === b.length && a.every((x, i) => x.pos === b[i].pos && Math.abs(x.height - b[i].height) < 1);
|
|
132
|
+
var DocumentPagination = Extension.create({
|
|
133
|
+
name: "juneDocPagination",
|
|
134
|
+
addProseMirrorPlugins() {
|
|
135
|
+
return [
|
|
136
|
+
new Plugin({
|
|
137
|
+
key: paginationKey,
|
|
138
|
+
state: {
|
|
139
|
+
init: () => ({ breaks: [], pages: 1, decorations: DecorationSet.empty, epoch: 0 }),
|
|
140
|
+
apply(tr, prev, _old, state) {
|
|
141
|
+
const meta = tr.getMeta(paginationKey);
|
|
142
|
+
if (meta?.remeasure) return { ...prev, epoch: prev.epoch + 1 };
|
|
143
|
+
if (meta?.breaks) {
|
|
144
|
+
return { ...prev, breaks: meta.breaks, pages: meta.pages ?? prev.pages, decorations: build(state.doc, meta.breaks) };
|
|
145
|
+
}
|
|
146
|
+
if (tr.docChanged) {
|
|
147
|
+
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);
|
|
148
|
+
return { ...prev, breaks, decorations: prev.decorations.map(tr.mapping, tr.doc) };
|
|
149
|
+
}
|
|
150
|
+
return prev;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
props: {
|
|
154
|
+
decorations: (state) => paginationKey.getState(state)?.decorations
|
|
155
|
+
},
|
|
156
|
+
view(view) {
|
|
157
|
+
let frame = null;
|
|
158
|
+
let rounds = 0;
|
|
159
|
+
const schedule = (reset) => {
|
|
160
|
+
if (reset) rounds = 0;
|
|
161
|
+
if (frame !== null || typeof window === "undefined") return;
|
|
162
|
+
const raf = window.requestAnimationFrame ?? ((cb) => window.setTimeout(() => cb(0), 16));
|
|
163
|
+
frame = raf(() => {
|
|
164
|
+
frame = null;
|
|
165
|
+
if (view.isDestroyed) return;
|
|
166
|
+
const cfg = configs.get(view);
|
|
167
|
+
const current = paginationKey.getState(view.state);
|
|
168
|
+
if (!current) return;
|
|
169
|
+
const next = cfg?.enabled ? measure(view, cfg) : { breaks: [], pages: 1 };
|
|
170
|
+
if (same(next.breaks, current.breaks) && next.pages === current.pages) return;
|
|
171
|
+
if (++rounds > 4) return;
|
|
172
|
+
view.dispatch(view.state.tr.setMeta(paginationKey, next).setMeta("addToHistory", false));
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
schedule(true);
|
|
176
|
+
return {
|
|
177
|
+
update(v, prevState) {
|
|
178
|
+
const pg = paginationKey.getState(v.state);
|
|
179
|
+
const prevPg = paginationKey.getState(prevState);
|
|
180
|
+
if (v.state.doc !== prevState.doc || pg?.epoch !== prevPg?.epoch) schedule(true);
|
|
181
|
+
else if (pg !== prevPg) schedule(false);
|
|
182
|
+
},
|
|
183
|
+
destroy() {
|
|
184
|
+
frame = null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
function setPaginationConfig(editor, cfg) {
|
|
193
|
+
if (editor.isDestroyed) return;
|
|
194
|
+
const prev = configs.get(editor.view);
|
|
195
|
+
configs.set(editor.view, cfg);
|
|
196
|
+
if (!prev || prev.enabled !== cfg.enabled || prev.pageHeight !== cfg.pageHeight || prev.gap !== cfg.gap || prev.padTop !== cfg.padTop || prev.padBottom !== cfg.padBottom) {
|
|
197
|
+
remeasurePagination(editor);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function remeasurePagination(editor) {
|
|
201
|
+
if (editor.isDestroyed) return;
|
|
202
|
+
const s = paginationKey.getState(editor.state);
|
|
203
|
+
if (!s) return;
|
|
204
|
+
editor.view.dispatch(editor.state.tr.setMeta(paginationKey, { remeasure: true }).setMeta("addToHistory", false));
|
|
205
|
+
}
|
|
206
|
+
function getPageCount(editor) {
|
|
207
|
+
return editor && paginationKey.getState(editor.state)?.pages || 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/components/document-editor/document-search.ts
|
|
211
|
+
import { Extension as Extension2 } from "@tiptap/core";
|
|
212
|
+
import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
|
|
213
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
|
|
214
|
+
var searchKey = new PluginKey2("juneDocSearch");
|
|
43
215
|
function findMatches(doc, term, caseSensitive) {
|
|
44
216
|
if (!term) return [];
|
|
45
217
|
const needle = caseSensitive ? term : term.toLowerCase();
|
|
@@ -68,32 +240,32 @@ function findMatches(doc, term, caseSensitive) {
|
|
|
68
240
|
});
|
|
69
241
|
return out;
|
|
70
242
|
}
|
|
71
|
-
function
|
|
243
|
+
function build2(doc, term, caseSensitive, current) {
|
|
72
244
|
const matches = findMatches(doc, term, caseSensitive);
|
|
73
245
|
const active = matches.length ? Math.min(Math.max(current, 0), matches.length - 1) : -1;
|
|
74
|
-
const decorations =
|
|
246
|
+
const decorations = DecorationSet2.create(
|
|
75
247
|
doc,
|
|
76
248
|
matches.map(
|
|
77
|
-
(m, i) =>
|
|
249
|
+
(m, i) => Decoration2.inline(m.from, m.to, {
|
|
78
250
|
class: i === active ? "june-doc__match june-doc__match--active" : "june-doc__match"
|
|
79
251
|
})
|
|
80
252
|
)
|
|
81
253
|
);
|
|
82
254
|
return { term, caseSensitive, current: active, matches, decorations };
|
|
83
255
|
}
|
|
84
|
-
var DocumentSearch =
|
|
256
|
+
var DocumentSearch = Extension2.create({
|
|
85
257
|
name: "juneDocSearch",
|
|
86
258
|
addProseMirrorPlugins() {
|
|
87
259
|
return [
|
|
88
|
-
new
|
|
260
|
+
new Plugin2({
|
|
89
261
|
key: searchKey,
|
|
90
262
|
state: {
|
|
91
|
-
init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations:
|
|
263
|
+
init: () => ({ term: "", caseSensitive: false, current: -1, matches: [], decorations: DecorationSet2.empty }),
|
|
92
264
|
apply(tr, prev, _old, state) {
|
|
93
265
|
const meta = tr.getMeta(searchKey);
|
|
94
266
|
if (!meta && !tr.docChanged) return prev;
|
|
95
267
|
if (!meta && !prev.term) return prev;
|
|
96
|
-
return
|
|
268
|
+
return build2(
|
|
97
269
|
state.doc,
|
|
98
270
|
meta?.term ?? prev.term,
|
|
99
271
|
meta?.caseSensitive ?? prev.caseSensitive,
|
|
@@ -201,6 +373,8 @@ function createDocumentExtensions() {
|
|
|
201
373
|
}),
|
|
202
374
|
PageBreak,
|
|
203
375
|
DocumentSearch,
|
|
376
|
+
DocumentPagination,
|
|
377
|
+
// inactive until the live editor sets a page geometry
|
|
204
378
|
Markdown
|
|
205
379
|
];
|
|
206
380
|
}
|
|
@@ -330,6 +504,9 @@ export {
|
|
|
330
504
|
slugify,
|
|
331
505
|
debounce,
|
|
332
506
|
sanitizeHref,
|
|
507
|
+
setPaginationConfig,
|
|
508
|
+
remeasurePagination,
|
|
509
|
+
getPageCount,
|
|
333
510
|
getSearchState,
|
|
334
511
|
setSearch,
|
|
335
512
|
gotoMatch,
|