@estokad/next 0.1.0
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/LICENSE +17 -0
- package/README.md +96 -0
- package/dist/components.d.ts +58 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +87 -0
- package/dist/components.js.map +1 -0
- package/dist/draft.d.ts +21 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +101 -0
- package/dist/draft.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay-route.d.ts +2 -0
- package/dist/overlay-route.d.ts.map +1 -0
- package/dist/overlay-route.js +38 -0
- package/dist/overlay-route.js.map +1 -0
- package/dist/overlay.bundle.js +535 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +26 -0
- package/dist/provider.js.map +1 -0
- package/dist/rich-text.d.ts +18 -0
- package/dist/rich-text.d.ts.map +1 -0
- package/dist/rich-text.js +68 -0
- package/dist/rich-text.js.map +1 -0
- package/dist/token.d.ts +23 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +139 -0
- package/dist/token.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Visual edit overlay (M3.3).
|
|
3
|
+
//
|
|
4
|
+
// Injected into the customer's site by @estokad/next/EstokadProvider when
|
|
5
|
+
// Next.js draft mode is on. Walks the DOM for `data-estokad-field`
|
|
6
|
+
// attributes (emitted by the <Estokad.*> components in M2.3), draws a
|
|
7
|
+
// hover outline, lets the editor click into a contenteditable wrapper,
|
|
8
|
+
// and posts field-changed events back to the Studio via postMessage.
|
|
9
|
+
//
|
|
10
|
+
// Per docs/visual-edit.md § 4 the wire protocol is:
|
|
11
|
+
//
|
|
12
|
+
// overlay → Studio: { type: 'estokad:overlay-ready', version }
|
|
13
|
+
// { type: 'estokad:field-changed', entry, field, value }
|
|
14
|
+
// { type: 'estokad:field-focused', entry, field }
|
|
15
|
+
// { type: 'estokad:field-blurred', entry, field }
|
|
16
|
+
// { type: 'estokad:navigation', from, to }
|
|
17
|
+
// Studio → overlay: { type: 'estokad:studio-ready', userId }
|
|
18
|
+
// { type: 'estokad:set-mode', mode: 'preview' | 'edit' }
|
|
19
|
+
// { type: 'estokad:focus-field', entry, field }
|
|
20
|
+
// { type: 'estokad:refresh', reason }
|
|
21
|
+
// { type: 'estokad:field-update', entry, field, value }
|
|
22
|
+
//
|
|
23
|
+
// Origin checking is strict on every message — we only trust messages from
|
|
24
|
+
// the studio origin recorded at handshake time.
|
|
25
|
+
const VERSION = '0.1.0';
|
|
26
|
+
const STUDIO_ORIGIN_COOKIE = '__estokad_studio_origin';
|
|
27
|
+
const FIELD_ATTR = 'data-estokad-field';
|
|
28
|
+
const ENTRY_ATTR = 'data-estokad-entry';
|
|
29
|
+
const FIELDTYPE_ATTR = 'data-estokad-fieldtype';
|
|
30
|
+
const DEBOUNCE_MS = 200;
|
|
31
|
+
const state = {
|
|
32
|
+
mode: 'preview',
|
|
33
|
+
studioOrigin: null,
|
|
34
|
+
outlineEl: null,
|
|
35
|
+
tooltipEl: null,
|
|
36
|
+
hoveredEl: null,
|
|
37
|
+
editingEl: null,
|
|
38
|
+
debounceTimer: null,
|
|
39
|
+
};
|
|
40
|
+
function init() {
|
|
41
|
+
state.studioOrigin = readCookie(STUDIO_ORIGIN_COOKIE);
|
|
42
|
+
if (!state.studioOrigin) {
|
|
43
|
+
// No studio origin → no parent to talk to. Stay dormant.
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
injectStyles();
|
|
47
|
+
state.outlineEl = createOutline();
|
|
48
|
+
state.tooltipEl = createTooltip();
|
|
49
|
+
document.body.appendChild(state.outlineEl);
|
|
50
|
+
document.body.appendChild(state.tooltipEl);
|
|
51
|
+
attachListeners();
|
|
52
|
+
observeMutations();
|
|
53
|
+
// Tell the Studio we're alive.
|
|
54
|
+
postToStudio({ type: 'estokad:overlay-ready', version: VERSION });
|
|
55
|
+
// Listen for the Studio side.
|
|
56
|
+
window.addEventListener('message', onParentMessage);
|
|
57
|
+
// Track SPA navigation so the Studio can update its iframe state.
|
|
58
|
+
let lastPath = window.location.pathname;
|
|
59
|
+
const checkNav = () => {
|
|
60
|
+
if (window.location.pathname !== lastPath) {
|
|
61
|
+
const from = lastPath;
|
|
62
|
+
lastPath = window.location.pathname;
|
|
63
|
+
postToStudio({ type: 'estokad:navigation', from, to: lastPath });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
window.addEventListener('popstate', checkNav);
|
|
67
|
+
// History.pushState/replaceState patch — small, unobtrusive.
|
|
68
|
+
patchHistory(checkNav);
|
|
69
|
+
}
|
|
70
|
+
function attachListeners() {
|
|
71
|
+
// Single delegated mousemove listener walks the path on each event;
|
|
72
|
+
// cheap enough at human-scale interaction rates.
|
|
73
|
+
document.addEventListener('mousemove', onMouseMove, true);
|
|
74
|
+
document.addEventListener('click', onClick, true);
|
|
75
|
+
window.addEventListener('scroll', repositionOutline, true);
|
|
76
|
+
window.addEventListener('resize', repositionOutline);
|
|
77
|
+
}
|
|
78
|
+
function onMouseMove(event) {
|
|
79
|
+
if (state.mode !== 'edit') {
|
|
80
|
+
hideOutline();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (state.editingEl)
|
|
84
|
+
return;
|
|
85
|
+
const target = closestEditable(event.target);
|
|
86
|
+
if (target === state.hoveredEl) {
|
|
87
|
+
if (target)
|
|
88
|
+
repositionOutline();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
state.hoveredEl = target;
|
|
92
|
+
if (target) {
|
|
93
|
+
showOutline(target);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
hideOutline();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function onClick(event) {
|
|
100
|
+
if (state.mode !== 'edit')
|
|
101
|
+
return;
|
|
102
|
+
const target = closestEditable(event.target);
|
|
103
|
+
if (!target)
|
|
104
|
+
return;
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
// Asset fields don't go contenteditable — they ask the Studio to open
|
|
108
|
+
// its asset picker modal. The Studio replies with field-update which
|
|
109
|
+
// we apply to the IMG src below.
|
|
110
|
+
if (target.getAttribute(FIELDTYPE_ATTR) === 'asset') {
|
|
111
|
+
postToStudio({
|
|
112
|
+
type: 'estokad:open-asset-picker',
|
|
113
|
+
entry: target.getAttribute(ENTRY_ATTR) ?? '',
|
|
114
|
+
field: target.getAttribute(FIELD_ATTR) ?? '',
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
beginEdit(target);
|
|
119
|
+
}
|
|
120
|
+
function beginEdit(el) {
|
|
121
|
+
if (state.editingEl)
|
|
122
|
+
endEdit({ commit: true });
|
|
123
|
+
state.editingEl = el;
|
|
124
|
+
el.setAttribute('contenteditable', 'true');
|
|
125
|
+
el.classList.add('estokad-editing');
|
|
126
|
+
el.focus();
|
|
127
|
+
hideOutline();
|
|
128
|
+
postToStudio({
|
|
129
|
+
type: 'estokad:field-focused',
|
|
130
|
+
entry: el.getAttribute(ENTRY_ATTR) ?? '',
|
|
131
|
+
field: el.getAttribute(FIELD_ATTR) ?? '',
|
|
132
|
+
});
|
|
133
|
+
el.addEventListener('input', onEditingInput);
|
|
134
|
+
el.addEventListener('blur', onEditingBlur);
|
|
135
|
+
el.addEventListener('keydown', onEditingKeyDown);
|
|
136
|
+
}
|
|
137
|
+
function endEdit(options) {
|
|
138
|
+
const el = state.editingEl;
|
|
139
|
+
if (!el)
|
|
140
|
+
return;
|
|
141
|
+
if (state.debounceTimer) {
|
|
142
|
+
clearTimeout(state.debounceTimer);
|
|
143
|
+
state.debounceTimer = null;
|
|
144
|
+
}
|
|
145
|
+
if (options.commit)
|
|
146
|
+
flushFieldChange(el);
|
|
147
|
+
el.removeAttribute('contenteditable');
|
|
148
|
+
el.classList.remove('estokad-editing');
|
|
149
|
+
el.removeEventListener('input', onEditingInput);
|
|
150
|
+
el.removeEventListener('blur', onEditingBlur);
|
|
151
|
+
el.removeEventListener('keydown', onEditingKeyDown);
|
|
152
|
+
postToStudio({
|
|
153
|
+
type: 'estokad:field-blurred',
|
|
154
|
+
entry: el.getAttribute(ENTRY_ATTR) ?? '',
|
|
155
|
+
field: el.getAttribute(FIELD_ATTR) ?? '',
|
|
156
|
+
});
|
|
157
|
+
state.editingEl = null;
|
|
158
|
+
}
|
|
159
|
+
function onEditingInput() {
|
|
160
|
+
const el = state.editingEl;
|
|
161
|
+
if (!el)
|
|
162
|
+
return;
|
|
163
|
+
if (state.debounceTimer)
|
|
164
|
+
clearTimeout(state.debounceTimer);
|
|
165
|
+
state.debounceTimer = setTimeout(() => flushFieldChange(el), DEBOUNCE_MS);
|
|
166
|
+
}
|
|
167
|
+
function onEditingBlur() {
|
|
168
|
+
endEdit({ commit: true });
|
|
169
|
+
}
|
|
170
|
+
function onEditingKeyDown(event) {
|
|
171
|
+
if (event.key === 'Escape') {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
endEdit({ commit: false });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const el = state.editingEl;
|
|
177
|
+
const isRichText = el?.getAttribute(FIELDTYPE_ATTR) === 'richText';
|
|
178
|
+
if (isRichText) {
|
|
179
|
+
// M3.8d — keyboard mark toggles. document.execCommand is deprecated
|
|
180
|
+
// but the only viable cross-browser path until we ship a real
|
|
181
|
+
// Tiptap-in-iframe bundle. The contenteditable wraps the selection
|
|
182
|
+
// in <strong>/<em>/<u>; the M3.8d serializer lifts those into
|
|
183
|
+
// ProseMirror marks at flush time.
|
|
184
|
+
const meta = event.metaKey || event.ctrlKey;
|
|
185
|
+
if (!meta)
|
|
186
|
+
return;
|
|
187
|
+
if (event.key === 'b' || event.key === 'B') {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
document.execCommand('bold');
|
|
190
|
+
onEditingInput();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (event.key === 'i' || event.key === 'I') {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
document.execCommand('italic');
|
|
196
|
+
onEditingInput();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (event.key === 'u' || event.key === 'U') {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
document.execCommand('underline');
|
|
202
|
+
onEditingInput();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (event.key === 'k' || event.key === 'K') {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
const href = window.prompt('Link URL:', 'https://');
|
|
208
|
+
if (href && href !== 'https://') {
|
|
209
|
+
document.execCommand('createLink', false, href);
|
|
210
|
+
onEditingInput();
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
217
|
+
// Single-line text: Enter commits + blurs.
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
endEdit({ commit: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function flushFieldChange(el) {
|
|
223
|
+
const entry = el.getAttribute(ENTRY_ATTR) ?? '';
|
|
224
|
+
const field = el.getAttribute(FIELD_ATTR) ?? '';
|
|
225
|
+
const fieldType = el.getAttribute(FIELDTYPE_ATTR);
|
|
226
|
+
// M3.8c → M3.8d — richText fields round-trip through Tiptap on the
|
|
227
|
+
// Studio side (hidden textarea holds JSON). The overlay walks the
|
|
228
|
+
// contenteditable's DOM and produces a ProseMirror doc that carries
|
|
229
|
+
// inline marks (bold, italic, underline, strike, code, link). Block
|
|
230
|
+
// structure is paragraph-only for now; full Tiptap nodes (lists,
|
|
231
|
+
// blockquotes, headings) would need a real Tiptap-in-iframe bundle.
|
|
232
|
+
if (fieldType === 'richText') {
|
|
233
|
+
const doc = serializeRichText(el);
|
|
234
|
+
postToStudio({
|
|
235
|
+
type: 'estokad:field-changed',
|
|
236
|
+
entry,
|
|
237
|
+
field,
|
|
238
|
+
fieldtype: 'richText',
|
|
239
|
+
value: JSON.stringify(doc),
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const value = el.textContent ?? '';
|
|
244
|
+
postToStudio({ type: 'estokad:field-changed', entry, field, value });
|
|
245
|
+
}
|
|
246
|
+
// DOM → ProseMirror walker. Splits into paragraphs on <br>/<p>/<div>
|
|
247
|
+
// boundaries, collects inline marks from ancestors. Empty paragraphs
|
|
248
|
+
// preserved so the editor can hold blank lines.
|
|
249
|
+
function serializeRichText(root) {
|
|
250
|
+
const paragraphs = [[]];
|
|
251
|
+
function pushText(text, marks) {
|
|
252
|
+
if (!text)
|
|
253
|
+
return;
|
|
254
|
+
const node = marks.length > 0 ? { type: 'text', text, marks } : { type: 'text', text };
|
|
255
|
+
paragraphs[paragraphs.length - 1].push(node);
|
|
256
|
+
}
|
|
257
|
+
function newParagraph() {
|
|
258
|
+
paragraphs.push([]);
|
|
259
|
+
}
|
|
260
|
+
function walk(node, marks) {
|
|
261
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
262
|
+
pushText(node.nodeValue ?? '', marks);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!(node instanceof Element))
|
|
266
|
+
return;
|
|
267
|
+
const tag = node.tagName.toLowerCase();
|
|
268
|
+
if (tag === 'br') {
|
|
269
|
+
newParagraph();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (tag === 'p' || tag === 'div') {
|
|
273
|
+
// Block boundary. Open a new paragraph unless current is empty
|
|
274
|
+
// (avoids leading-blank-paragraph noise from contenteditable).
|
|
275
|
+
if (paragraphs[paragraphs.length - 1].length > 0)
|
|
276
|
+
newParagraph();
|
|
277
|
+
for (const child of Array.from(node.childNodes))
|
|
278
|
+
walk(child, marks);
|
|
279
|
+
newParagraph();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const nextMarks = markFor(node, marks);
|
|
283
|
+
for (const child of Array.from(node.childNodes))
|
|
284
|
+
walk(child, nextMarks);
|
|
285
|
+
}
|
|
286
|
+
for (const child of Array.from(root.childNodes))
|
|
287
|
+
walk(child, []);
|
|
288
|
+
return {
|
|
289
|
+
type: 'doc',
|
|
290
|
+
content: paragraphs.map((nodes) => ({
|
|
291
|
+
type: 'paragraph',
|
|
292
|
+
...(nodes.length > 0 && { content: nodes }),
|
|
293
|
+
})),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function markFor(el, inherited) {
|
|
297
|
+
const tag = el.tagName.toLowerCase();
|
|
298
|
+
switch (tag) {
|
|
299
|
+
case 'strong':
|
|
300
|
+
case 'b':
|
|
301
|
+
return appendMark(inherited, { type: 'bold' });
|
|
302
|
+
case 'em':
|
|
303
|
+
case 'i':
|
|
304
|
+
return appendMark(inherited, { type: 'italic' });
|
|
305
|
+
case 'u':
|
|
306
|
+
return appendMark(inherited, { type: 'underline' });
|
|
307
|
+
case 's':
|
|
308
|
+
case 'strike':
|
|
309
|
+
case 'del':
|
|
310
|
+
return appendMark(inherited, { type: 'strike' });
|
|
311
|
+
case 'code':
|
|
312
|
+
return appendMark(inherited, { type: 'code' });
|
|
313
|
+
case 'a': {
|
|
314
|
+
const href = el.getAttribute('href');
|
|
315
|
+
return appendMark(inherited, { type: 'link', attrs: { href: href ?? '#' } });
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
return inherited;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function appendMark(marks, mark) {
|
|
322
|
+
if (marks.some((m) => m.type === mark.type))
|
|
323
|
+
return marks;
|
|
324
|
+
return [...marks, mark];
|
|
325
|
+
}
|
|
326
|
+
// --- outline + tooltip ---------------------------------------------------
|
|
327
|
+
function createOutline() {
|
|
328
|
+
const el = document.createElement('div');
|
|
329
|
+
el.className = 'estokad-outline';
|
|
330
|
+
el.style.display = 'none';
|
|
331
|
+
return el;
|
|
332
|
+
}
|
|
333
|
+
function createTooltip() {
|
|
334
|
+
const el = document.createElement('div');
|
|
335
|
+
el.className = 'estokad-tooltip';
|
|
336
|
+
el.style.display = 'none';
|
|
337
|
+
return el;
|
|
338
|
+
}
|
|
339
|
+
function showOutline(target) {
|
|
340
|
+
const outline = state.outlineEl;
|
|
341
|
+
const tooltip = state.tooltipEl;
|
|
342
|
+
if (!outline || !tooltip)
|
|
343
|
+
return;
|
|
344
|
+
const rect = target.getBoundingClientRect();
|
|
345
|
+
positionOverlay(outline, rect);
|
|
346
|
+
outline.style.display = 'block';
|
|
347
|
+
const fieldName = target.getAttribute(FIELD_ATTR) ?? '';
|
|
348
|
+
tooltip.textContent = `→ ${fieldName}`;
|
|
349
|
+
tooltip.style.left = `${rect.left + window.scrollX}px`;
|
|
350
|
+
tooltip.style.top = `${rect.top + window.scrollY - 22}px`;
|
|
351
|
+
tooltip.style.display = 'block';
|
|
352
|
+
}
|
|
353
|
+
function hideOutline() {
|
|
354
|
+
if (state.outlineEl)
|
|
355
|
+
state.outlineEl.style.display = 'none';
|
|
356
|
+
if (state.tooltipEl)
|
|
357
|
+
state.tooltipEl.style.display = 'none';
|
|
358
|
+
state.hoveredEl = null;
|
|
359
|
+
}
|
|
360
|
+
function repositionOutline() {
|
|
361
|
+
if (!state.hoveredEl)
|
|
362
|
+
return;
|
|
363
|
+
const outline = state.outlineEl;
|
|
364
|
+
const tooltip = state.tooltipEl;
|
|
365
|
+
if (!outline || !tooltip)
|
|
366
|
+
return;
|
|
367
|
+
const rect = state.hoveredEl.getBoundingClientRect();
|
|
368
|
+
positionOverlay(outline, rect);
|
|
369
|
+
tooltip.style.left = `${rect.left + window.scrollX}px`;
|
|
370
|
+
tooltip.style.top = `${rect.top + window.scrollY - 22}px`;
|
|
371
|
+
}
|
|
372
|
+
function positionOverlay(el, rect) {
|
|
373
|
+
el.style.left = `${rect.left + window.scrollX}px`;
|
|
374
|
+
el.style.top = `${rect.top + window.scrollY}px`;
|
|
375
|
+
el.style.width = `${rect.width}px`;
|
|
376
|
+
el.style.height = `${rect.height}px`;
|
|
377
|
+
}
|
|
378
|
+
// --- DOM walker ----------------------------------------------------------
|
|
379
|
+
function closestEditable(target) {
|
|
380
|
+
let node = target;
|
|
381
|
+
while (node && node !== document.body) {
|
|
382
|
+
if (node instanceof HTMLElement && node.hasAttribute(FIELD_ATTR))
|
|
383
|
+
return node;
|
|
384
|
+
node = node.parentElement;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
function observeMutations() {
|
|
389
|
+
const observer = new MutationObserver(() => {
|
|
390
|
+
// No per-element listener attachment needed (we use document-level
|
|
391
|
+
// delegation). The observer exists so we can hide the outline if the
|
|
392
|
+
// currently hovered element is removed by a route change.
|
|
393
|
+
if (state.hoveredEl && !document.body.contains(state.hoveredEl)) {
|
|
394
|
+
hideOutline();
|
|
395
|
+
}
|
|
396
|
+
if (state.editingEl && !document.body.contains(state.editingEl)) {
|
|
397
|
+
state.editingEl = null;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
401
|
+
}
|
|
402
|
+
// --- bridge --------------------------------------------------------------
|
|
403
|
+
function postToStudio(message) {
|
|
404
|
+
if (!state.studioOrigin)
|
|
405
|
+
return;
|
|
406
|
+
if (window.parent === window)
|
|
407
|
+
return; // not iframed
|
|
408
|
+
window.parent.postMessage(message, state.studioOrigin);
|
|
409
|
+
}
|
|
410
|
+
function onParentMessage(event) {
|
|
411
|
+
if (!state.studioOrigin || event.origin !== state.studioOrigin)
|
|
412
|
+
return;
|
|
413
|
+
if (typeof event.data !== 'object' || event.data === null)
|
|
414
|
+
return;
|
|
415
|
+
const msg = event.data;
|
|
416
|
+
switch (msg.type) {
|
|
417
|
+
case 'estokad:studio-ready':
|
|
418
|
+
// Acknowledge nothing — handshake complete.
|
|
419
|
+
break;
|
|
420
|
+
case 'estokad:set-mode':
|
|
421
|
+
if (msg.mode === 'preview' || msg.mode === 'edit') {
|
|
422
|
+
state.mode = msg.mode;
|
|
423
|
+
if (msg.mode === 'preview') {
|
|
424
|
+
if (state.editingEl)
|
|
425
|
+
endEdit({ commit: true });
|
|
426
|
+
hideOutline();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
case 'estokad:focus-field':
|
|
431
|
+
if (typeof msg.field === 'string') {
|
|
432
|
+
const el = document.querySelector(`[${FIELD_ATTR}="${cssEscape(msg.field)}"]`);
|
|
433
|
+
if (el) {
|
|
434
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
435
|
+
beginEdit(el);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
case 'estokad:field-update':
|
|
440
|
+
if (typeof msg.field === 'string' && typeof msg.value === 'string') {
|
|
441
|
+
const sel = `[${FIELD_ATTR}="${cssEscape(msg.field)}"]`;
|
|
442
|
+
const el = document.querySelector(sel);
|
|
443
|
+
if (!el || el === state.editingEl)
|
|
444
|
+
break;
|
|
445
|
+
if (el.getAttribute(FIELDTYPE_ATTR) === 'asset' && el instanceof HTMLImageElement) {
|
|
446
|
+
// Asset fields update via the IMG src.
|
|
447
|
+
el.src = msg.value;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
el.textContent = msg.value;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
case 'estokad:refresh':
|
|
455
|
+
window.location.reload();
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// --- helpers -------------------------------------------------------------
|
|
460
|
+
function readCookie(name) {
|
|
461
|
+
const match = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`));
|
|
462
|
+
if (!match)
|
|
463
|
+
return null;
|
|
464
|
+
const value = match.slice(name.length + 1);
|
|
465
|
+
try {
|
|
466
|
+
return decodeURIComponent(value);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function cssEscape(s) {
|
|
473
|
+
// Native CSS.escape exists in all current browsers; use a tiny fallback
|
|
474
|
+
// for the unlikely case it's missing (older WebViews).
|
|
475
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function')
|
|
476
|
+
return CSS.escape(s);
|
|
477
|
+
return s.replace(/(["'\\\][])/g, '\\$1');
|
|
478
|
+
}
|
|
479
|
+
function patchHistory(cb) {
|
|
480
|
+
const orig = { push: history.pushState, replace: history.replaceState };
|
|
481
|
+
history.pushState = function (...args) {
|
|
482
|
+
const r = orig.push.apply(history, args);
|
|
483
|
+
cb();
|
|
484
|
+
return r;
|
|
485
|
+
};
|
|
486
|
+
history.replaceState = function (...args) {
|
|
487
|
+
const r = orig.replace.apply(history, args);
|
|
488
|
+
cb();
|
|
489
|
+
return r;
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function injectStyles() {
|
|
493
|
+
const css = `
|
|
494
|
+
:where(.estokad-outline) {
|
|
495
|
+
position: absolute;
|
|
496
|
+
pointer-events: none;
|
|
497
|
+
border: 1.5px dashed #00D4FF;
|
|
498
|
+
border-radius: 2px;
|
|
499
|
+
z-index: 999998;
|
|
500
|
+
box-sizing: border-box;
|
|
501
|
+
}
|
|
502
|
+
:where(.estokad-tooltip) {
|
|
503
|
+
position: absolute;
|
|
504
|
+
pointer-events: none;
|
|
505
|
+
z-index: 999999;
|
|
506
|
+
background: #00D4FF;
|
|
507
|
+
color: #0a0d10;
|
|
508
|
+
padding: 2px 8px;
|
|
509
|
+
border-radius: 2px;
|
|
510
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
511
|
+
font-size: 11px;
|
|
512
|
+
font-weight: 500;
|
|
513
|
+
letter-spacing: 0.04em;
|
|
514
|
+
white-space: nowrap;
|
|
515
|
+
}
|
|
516
|
+
:where(.estokad-editing) {
|
|
517
|
+
outline: 1.5px solid #00D4FF !important;
|
|
518
|
+
outline-offset: 2px;
|
|
519
|
+
border-radius: 2px;
|
|
520
|
+
caret-color: #00D4FF;
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
const style = document.createElement('style');
|
|
524
|
+
style.setAttribute('data-estokad-overlay', '');
|
|
525
|
+
style.textContent = css;
|
|
526
|
+
document.head.appendChild(style);
|
|
527
|
+
}
|
|
528
|
+
// --- bootstrap -----------------------------------------------------------
|
|
529
|
+
if (document.readyState === 'loading') {
|
|
530
|
+
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
init();
|
|
534
|
+
}
|
|
535
|
+
//# sourceMappingURL=overlay.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
readonly children: ReactNode;
|
|
4
|
+
/** Override the route the overlay script loads from. Defaults to
|
|
5
|
+
* `/api/estokad/overlay`, which the customer wires up by exporting
|
|
6
|
+
* `estokadOverlayRoute` as the GET handler at that path. */
|
|
7
|
+
readonly overlaySrc?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function EstokadProvider({ children, overlaySrc }: Props): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,UAAU,KAAK;IACb,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B;;iEAE6D;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,wBAAsB,eAAe,CAAC,EAAE,QAAQ,EAAE,UAAmC,EAAE,EAAE,KAAK,oDAS7F"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// EstokadProvider — wraps the customer's app in `app/layout.tsx` and
|
|
3
|
+
// injects the visual-edit overlay script when Next.js draft mode is on.
|
|
4
|
+
//
|
|
5
|
+
// import { EstokadProvider } from '@estokad/next'
|
|
6
|
+
//
|
|
7
|
+
// export default function RootLayout({ children }) {
|
|
8
|
+
// return (
|
|
9
|
+
// <html>
|
|
10
|
+
// <body>
|
|
11
|
+
// <EstokadProvider>{children}</EstokadProvider>
|
|
12
|
+
// </body>
|
|
13
|
+
// </html>
|
|
14
|
+
// )
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// In production (no draft cookie) this is a pass-through — zero overhead.
|
|
18
|
+
// In draft mode it renders a `<script type="module" src="...">` that
|
|
19
|
+
// loads the overlay bundle via the customer's own /api/estokad/overlay
|
|
20
|
+
// route handler (which is mounted with `estokadOverlayRoute`).
|
|
21
|
+
import { draftMode } from 'next/headers';
|
|
22
|
+
export async function EstokadProvider({ children, overlaySrc = '/api/estokad/overlay' }) {
|
|
23
|
+
const { isEnabled } = await draftMode();
|
|
24
|
+
return (_jsxs(_Fragment, { children: [children, isEnabled ? _jsx("script", { type: "module", src: overlaySrc, async: true }) : null] }));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":";AAAA,qEAAqE;AACrE,wEAAwE;AACxE,EAAE;AACF,oDAAoD;AACpD,EAAE;AACF,uDAAuD;AACvD,eAAe;AACf,eAAe;AACf,iBAAiB;AACjB,0DAA0D;AAC1D,kBAAkB;AAClB,gBAAgB;AAChB,QAAQ;AACR,MAAM;AACN,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,uEAAuE;AACvE,+DAA+D;AAE/D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAWxC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EAAE,QAAQ,EAAE,UAAU,GAAG,sBAAsB,EAAS;IAC5F,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,EAAE,CAAA;IAEvC,OAAO,CACL,8BACG,QAAQ,EACR,SAAS,CAAC,CAAC,CAAC,iBAAQ,IAAI,EAAC,QAAQ,EAAC,GAAG,EAAE,UAAU,EAAE,KAAK,SAAG,CAAC,CAAC,CAAC,IAAI,IAClE,CACJ,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface TiptapDoc {
|
|
3
|
+
readonly type: 'doc';
|
|
4
|
+
readonly content?: ReadonlyArray<TiptapNode>;
|
|
5
|
+
}
|
|
6
|
+
export interface TiptapNode {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly attrs?: Record<string, unknown>;
|
|
9
|
+
readonly content?: ReadonlyArray<TiptapNode>;
|
|
10
|
+
readonly text?: string;
|
|
11
|
+
readonly marks?: ReadonlyArray<TiptapMark>;
|
|
12
|
+
}
|
|
13
|
+
export interface TiptapMark {
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly attrs?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function renderRichText(doc: TiptapDoc): ReactNode;
|
|
18
|
+
//# sourceMappingURL=rich-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-text.d.ts","sourceRoot":"","sources":["../src/rich-text.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;IAC5C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,CAGxD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function renderRichText(doc) {
|
|
3
|
+
if (doc?.type !== 'doc' || !doc.content)
|
|
4
|
+
return null;
|
|
5
|
+
return doc.content.map((node, i) => renderNode(node, `n${i}`));
|
|
6
|
+
}
|
|
7
|
+
function renderNode(node, key) {
|
|
8
|
+
switch (node.type) {
|
|
9
|
+
case 'paragraph':
|
|
10
|
+
return _jsx("p", { children: renderChildren(node) }, key);
|
|
11
|
+
case 'heading': {
|
|
12
|
+
const level = node.attrs?.['level'] ?? 2;
|
|
13
|
+
const Tag = `h${Math.min(Math.max(level, 1), 6)}`;
|
|
14
|
+
return _jsx(Tag, { children: renderChildren(node) }, key);
|
|
15
|
+
}
|
|
16
|
+
case 'text':
|
|
17
|
+
return renderText(node, key);
|
|
18
|
+
case 'bulletList':
|
|
19
|
+
return _jsx("ul", { children: renderChildren(node) }, key);
|
|
20
|
+
case 'orderedList':
|
|
21
|
+
return _jsx("ol", { children: renderChildren(node) }, key);
|
|
22
|
+
case 'listItem':
|
|
23
|
+
return _jsx("li", { children: renderChildren(node) }, key);
|
|
24
|
+
case 'blockquote':
|
|
25
|
+
return _jsx("blockquote", { children: renderChildren(node) }, key);
|
|
26
|
+
case 'codeBlock':
|
|
27
|
+
return (_jsx("pre", { children: _jsx("code", { children: renderChildren(node) }) }, key));
|
|
28
|
+
case 'horizontalRule':
|
|
29
|
+
return _jsx("hr", {}, key);
|
|
30
|
+
case 'hardBreak':
|
|
31
|
+
return _jsx("br", {}, key);
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function renderChildren(node) {
|
|
37
|
+
if (!node.content)
|
|
38
|
+
return null;
|
|
39
|
+
return node.content.map((child, i) => renderNode(child, `n${i}`));
|
|
40
|
+
}
|
|
41
|
+
function renderText(node, key) {
|
|
42
|
+
let element = node.text ?? '';
|
|
43
|
+
for (const mark of node.marks ?? []) {
|
|
44
|
+
element = wrapMark(mark, element);
|
|
45
|
+
}
|
|
46
|
+
return _jsx("span", { children: element }, key);
|
|
47
|
+
}
|
|
48
|
+
function wrapMark(mark, child) {
|
|
49
|
+
switch (mark.type) {
|
|
50
|
+
case 'bold':
|
|
51
|
+
return _jsx("strong", { children: child });
|
|
52
|
+
case 'italic':
|
|
53
|
+
return _jsx("em", { children: child });
|
|
54
|
+
case 'strike':
|
|
55
|
+
return _jsx("s", { children: child });
|
|
56
|
+
case 'underline':
|
|
57
|
+
return _jsx("u", { children: child });
|
|
58
|
+
case 'code':
|
|
59
|
+
return _jsx("code", { children: child });
|
|
60
|
+
case 'link': {
|
|
61
|
+
const href = String(mark.attrs?.['href'] ?? '#');
|
|
62
|
+
return (_jsx("a", { href: href, rel: "noreferrer", children: child }));
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
return child;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=rich-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-text.js","sourceRoot":"","sources":["../src/rich-text.tsx"],"names":[],"mappings":"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"}
|