@lionrapid/core 0.1.2 → 0.1.4-beta.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/dist/browser/editor/editor-api.d.ts +61 -0
- package/dist/browser/editor/editor-api.d.ts.map +1 -1
- package/dist/browser/editor/editor-api.js +165 -0
- package/dist/browser/editor/editor-api.js.map +1 -1
- package/dist/browser/editor/editor-init.js +119 -7
- package/dist/browser/editor/editor-init.js.map +1 -1
- package/dist/browser/editor/editor-styles.d.ts +1 -1
- package/dist/browser/editor/editor-styles.d.ts.map +1 -1
- package/dist/browser/editor/editor-styles.js +81 -0
- package/dist/browser/editor/editor-styles.js.map +1 -1
- package/dist/browser/editor/inspector.d.ts +5 -1
- package/dist/browser/editor/inspector.d.ts.map +1 -1
- package/dist/browser/editor/inspector.js +32 -1
- package/dist/browser/editor/inspector.js.map +1 -1
- package/dist/browser/editor/pii-redactor.d.ts +39 -0
- package/dist/browser/editor/pii-redactor.d.ts.map +1 -0
- package/dist/browser/editor/pii-redactor.js +149 -0
- package/dist/browser/editor/pii-redactor.js.map +1 -0
- package/dist/browser/editor/sidebar.d.ts +17 -1
- package/dist/browser/editor/sidebar.d.ts.map +1 -1
- package/dist/browser/editor/sidebar.js +77 -1
- package/dist/browser/editor/sidebar.js.map +1 -1
- package/dist/browser/editor/snapshot-capture.d.ts +57 -0
- package/dist/browser/editor/snapshot-capture.d.ts.map +1 -0
- package/dist/browser/editor/snapshot-capture.js +235 -0
- package/dist/browser/editor/snapshot-capture.js.map +1 -0
- package/dist/browser/editor/snapshot-viewer.d.ts +35 -0
- package/dist/browser/editor/snapshot-viewer.d.ts.map +1 -0
- package/dist/browser/editor/snapshot-viewer.js +90 -0
- package/dist/browser/editor/snapshot-viewer.js.map +1 -0
- package/dist/browser/editor/toolbar.d.ts +7 -1
- package/dist/browser/editor/toolbar.d.ts.map +1 -1
- package/dist/browser/editor/toolbar.js +26 -1
- package/dist/browser/editor/toolbar.js.map +1 -1
- package/dist/browser/lionrapid.editor.js +707 -8
- package/dist/browser/lionrapid.editor.js.map +4 -4
- package/dist/browser/lionrapid.editor.min.js +84 -2
- package/dist/browser/lionrapid.editor.min.js.map +4 -4
- package/package.json +1 -1
|
@@ -153,6 +153,87 @@ var LionRapidEditor = (() => {
|
|
|
153
153
|
background: rgba(255,255,255,0.1);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
.lr-fab-snapshot {
|
|
157
|
+
background: #2d7f7f;
|
|
158
|
+
border: 1px solid #2d7f7f;
|
|
159
|
+
color: #fff;
|
|
160
|
+
padding: 4px 10px;
|
|
161
|
+
border-radius: 4px;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
font-size: 13px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.lr-fab-snapshot:hover {
|
|
168
|
+
background: #3a9999;
|
|
169
|
+
border-color: #3a9999;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.lr-fab-snapshot:disabled {
|
|
173
|
+
opacity: 0.5;
|
|
174
|
+
cursor: not-allowed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Workflow action buttons (Phase 3) */
|
|
178
|
+
.lr-workflow-actions {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-wrap: wrap;
|
|
181
|
+
gap: 6px;
|
|
182
|
+
margin: 12px 0 4px;
|
|
183
|
+
padding: 8px 0 0;
|
|
184
|
+
border-top: 1px solid #e5e7eb;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.lr-wf-btn {
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
padding: 5px 10px;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
border: 1px solid transparent;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
transition: background-color 0.1s;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.lr-wf-btn:disabled {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
cursor: not-allowed;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.lr-wf-primary {
|
|
203
|
+
background: #2563eb;
|
|
204
|
+
color: white;
|
|
205
|
+
border-color: #2563eb;
|
|
206
|
+
}
|
|
207
|
+
.lr-wf-primary:hover:not(:disabled) {
|
|
208
|
+
background: #1d4ed8;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.lr-wf-secondary {
|
|
212
|
+
background: white;
|
|
213
|
+
color: #374151;
|
|
214
|
+
border-color: #d1d5db;
|
|
215
|
+
}
|
|
216
|
+
.lr-wf-secondary:hover:not(:disabled) {
|
|
217
|
+
background: #f3f4f6;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.lr-wf-danger {
|
|
221
|
+
background: white;
|
|
222
|
+
color: #b91c1c;
|
|
223
|
+
border-color: #fca5a5;
|
|
224
|
+
}
|
|
225
|
+
.lr-wf-danger:hover:not(:disabled) {
|
|
226
|
+
background: #fef2f2;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Snapshot viewer sandbox wrapper */
|
|
230
|
+
#__lr_snapshot_root {
|
|
231
|
+
isolation: isolate;
|
|
232
|
+
position: relative;
|
|
233
|
+
width: 100%;
|
|
234
|
+
min-height: 100vh;
|
|
235
|
+
}
|
|
236
|
+
|
|
156
237
|
/* \u2500\u2500 Overlay \u2500\u2500 */
|
|
157
238
|
.lr-overlay {
|
|
158
239
|
position: fixed;
|
|
@@ -733,13 +814,19 @@ var LionRapidEditor = (() => {
|
|
|
733
814
|
var SKIP_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "CODE", "PRE", "TEXTAREA", "NOSCRIPT", "SVG"]);
|
|
734
815
|
var BLOCK_SELECTOR2 = "p,h1,h2,h3,h4,h5,h6,li,td,th,label,button,figcaption,dt,dd,summary,caption";
|
|
735
816
|
var Inspector = class {
|
|
736
|
-
constructor(shadowRoot, onSelect) {
|
|
817
|
+
constructor(shadowRoot, onSelect, rootElement, snapshotKeyMap) {
|
|
737
818
|
__publicField(this, "overlay");
|
|
738
819
|
__publicField(this, "currentTarget", null);
|
|
739
820
|
__publicField(this, "onSelect");
|
|
740
821
|
__publicField(this, "rafId", null);
|
|
741
822
|
__publicField(this, "active", true);
|
|
823
|
+
__publicField(this, "rootElement");
|
|
824
|
+
/** When set, clicks resolve keys from this map (snapshot viewer mode)
|
|
825
|
+
* instead of re-computing from the (redacted) DOM text. */
|
|
826
|
+
__publicField(this, "snapshotKeyMap");
|
|
742
827
|
this.onSelect = onSelect;
|
|
828
|
+
this.rootElement = rootElement || document.body;
|
|
829
|
+
this.snapshotKeyMap = snapshotKeyMap || null;
|
|
743
830
|
this.overlay = document.createElement("div");
|
|
744
831
|
this.overlay.className = "lr-overlay";
|
|
745
832
|
shadowRoot.appendChild(this.overlay);
|
|
@@ -769,6 +856,7 @@ var LionRapidEditor = (() => {
|
|
|
769
856
|
const target = e.target;
|
|
770
857
|
if (!target || !target.closest) return;
|
|
771
858
|
if (target.closest("#__lionrapid-editor")) return;
|
|
859
|
+
if (!this.rootElement.contains(target)) return;
|
|
772
860
|
if (this.shouldSkip(target)) {
|
|
773
861
|
this.clearHighlight();
|
|
774
862
|
return;
|
|
@@ -787,11 +875,25 @@ var LionRapidEditor = (() => {
|
|
|
787
875
|
const target = e.target;
|
|
788
876
|
if (!target || !target.closest) return;
|
|
789
877
|
if (target.closest("#__lionrapid-editor")) return;
|
|
878
|
+
if (!this.rootElement.contains(target)) return;
|
|
790
879
|
const block = target.closest(BLOCK_SELECTOR2) || target.closest("[data-lr-block]");
|
|
791
880
|
if (!block) return;
|
|
792
881
|
if (this.shouldSkip(block)) return;
|
|
793
882
|
e.preventDefault();
|
|
794
883
|
e.stopPropagation();
|
|
884
|
+
if (this.snapshotKeyMap) {
|
|
885
|
+
const idxAttr = block.getAttribute("data-lr-snap-idx");
|
|
886
|
+
if (idxAttr !== null) {
|
|
887
|
+
const idx = parseInt(idxAttr, 10);
|
|
888
|
+
const key = this.snapshotKeyMap.get(idx);
|
|
889
|
+
if (key) {
|
|
890
|
+
const unit = parseBlockElement(block);
|
|
891
|
+
this.onSelect({ key, unit, translatedUnit: null, element: block });
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
795
897
|
const resolved = resolveKey(block);
|
|
796
898
|
if (resolved) {
|
|
797
899
|
this.onSelect(resolved);
|
|
@@ -828,7 +930,7 @@ var LionRapidEditor = (() => {
|
|
|
828
930
|
|
|
829
931
|
// src/browser/editor/toolbar.ts
|
|
830
932
|
var Toolbar = class {
|
|
831
|
-
constructor(shadowRoot, languages, currentLocale, onLanguageChange, onExit) {
|
|
933
|
+
constructor(shadowRoot, languages, currentLocale, onLanguageChange, onExit, onCaptureSnapshot = null, captureOptions = {}) {
|
|
832
934
|
__publicField(this, "el");
|
|
833
935
|
__publicField(this, "langBtnEl");
|
|
834
936
|
__publicField(this, "langMenuEl");
|
|
@@ -837,13 +939,16 @@ var LionRapidEditor = (() => {
|
|
|
837
939
|
__publicField(this, "currentLocale");
|
|
838
940
|
__publicField(this, "onLanguageChange");
|
|
839
941
|
__publicField(this, "onExit");
|
|
942
|
+
__publicField(this, "onCaptureSnapshot");
|
|
840
943
|
// Drag state
|
|
841
944
|
__publicField(this, "dragging", false);
|
|
842
945
|
__publicField(this, "dragOffsetX", 0);
|
|
843
946
|
__publicField(this, "dragOffsetY", 0);
|
|
844
947
|
this.onLanguageChange = onLanguageChange;
|
|
845
948
|
this.onExit = onExit;
|
|
949
|
+
this.onCaptureSnapshot = onCaptureSnapshot;
|
|
846
950
|
this.currentLocale = currentLocale;
|
|
951
|
+
const captureMode = captureOptions.mode ?? "capture";
|
|
847
952
|
this.el = document.createElement("div");
|
|
848
953
|
this.el.className = "lr-fab";
|
|
849
954
|
const fabBtn = document.createElement("button");
|
|
@@ -887,6 +992,22 @@ var LionRapidEditor = (() => {
|
|
|
887
992
|
}
|
|
888
993
|
langWrap.appendChild(this.langMenuEl);
|
|
889
994
|
panel.appendChild(langWrap);
|
|
995
|
+
if (this.onCaptureSnapshot) {
|
|
996
|
+
const captureBtn = document.createElement("button");
|
|
997
|
+
captureBtn.className = "lr-fab-snapshot";
|
|
998
|
+
if (captureMode === "recapture") {
|
|
999
|
+
captureBtn.textContent = "Save re-capture";
|
|
1000
|
+
captureBtn.title = "Save this page as a new version of the existing snapshot";
|
|
1001
|
+
} else {
|
|
1002
|
+
captureBtn.textContent = "Capture";
|
|
1003
|
+
captureBtn.title = "Capture a redacted snapshot of this page for translators";
|
|
1004
|
+
}
|
|
1005
|
+
captureBtn.addEventListener("click", (e) => {
|
|
1006
|
+
e.stopPropagation();
|
|
1007
|
+
if (this.onCaptureSnapshot) this.onCaptureSnapshot();
|
|
1008
|
+
});
|
|
1009
|
+
panel.appendChild(captureBtn);
|
|
1010
|
+
}
|
|
890
1011
|
this.progressEl = document.createElement("span");
|
|
891
1012
|
this.progressEl.className = "lr-fab-progress";
|
|
892
1013
|
panel.appendChild(this.progressEl);
|
|
@@ -1205,7 +1326,7 @@ var LionRapidEditor = (() => {
|
|
|
1205
1326
|
// src/browser/editor/sidebar.ts
|
|
1206
1327
|
var htmlSerializer2 = new HTMLSerializer();
|
|
1207
1328
|
var Sidebar = class {
|
|
1208
|
-
constructor(shadowRoot, api, locale, namespace, defaultLocale, onBlockClick) {
|
|
1329
|
+
constructor(shadowRoot, api, locale, namespace, defaultLocale, onBlockClick, userRole = "") {
|
|
1209
1330
|
__publicField(this, "el");
|
|
1210
1331
|
__publicField(this, "bodyEl");
|
|
1211
1332
|
__publicField(this, "statusEl");
|
|
@@ -1221,11 +1342,13 @@ var LionRapidEditor = (() => {
|
|
|
1221
1342
|
__publicField(this, "onProgressUpdate", null);
|
|
1222
1343
|
__publicField(this, "lastFetchTime", 0);
|
|
1223
1344
|
__publicField(this, "lastFlushTime", 0);
|
|
1345
|
+
__publicField(this, "userRole");
|
|
1224
1346
|
this.api = api;
|
|
1225
1347
|
this.locale = locale;
|
|
1226
1348
|
this.namespace = namespace;
|
|
1227
1349
|
this.defaultLocale = defaultLocale;
|
|
1228
1350
|
this.onBlockClick = onBlockClick;
|
|
1351
|
+
this.userRole = userRole;
|
|
1229
1352
|
this.blockExtractor = new BlockExtractor();
|
|
1230
1353
|
this.el = document.createElement("div");
|
|
1231
1354
|
this.el.className = "lr-sidebar";
|
|
@@ -1379,6 +1502,9 @@ var LionRapidEditor = (() => {
|
|
|
1379
1502
|
}
|
|
1380
1503
|
return previewEl;
|
|
1381
1504
|
});
|
|
1505
|
+
if (!isSourceLocale) {
|
|
1506
|
+
this.renderWorkflowActions(key, status);
|
|
1507
|
+
}
|
|
1382
1508
|
if (!isLocked) {
|
|
1383
1509
|
textarea.addEventListener("input", () => {
|
|
1384
1510
|
const text = textarea.value;
|
|
@@ -1401,6 +1527,69 @@ var LionRapidEditor = (() => {
|
|
|
1401
1527
|
setTimeout(() => textarea.focus(), 100);
|
|
1402
1528
|
}
|
|
1403
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Render Confirm/Reject/Mark-Ready/Unmark-Ready buttons under the preview.
|
|
1532
|
+
* Visibility:
|
|
1533
|
+
* - Confirm: shown when row is not locked and not already ready
|
|
1534
|
+
* - Reject: shown when row is not locked and not already ready (server
|
|
1535
|
+
* returns 400 if stage_index is already 0)
|
|
1536
|
+
* - Unconfirm: shown when isConfirmed
|
|
1537
|
+
* - Mark Ready: PM/admin only, hidden when already ready
|
|
1538
|
+
* - Unmark Ready: PM/admin only, only when ready
|
|
1539
|
+
* After any successful click we re-fetch status for this key so the UI
|
|
1540
|
+
* reflects the new state and the unaffected buttons hide/show themselves.
|
|
1541
|
+
*/
|
|
1542
|
+
renderWorkflowActions(key, status) {
|
|
1543
|
+
const isPM = this.userRole === "project_manager" || this.userRole === "admin";
|
|
1544
|
+
const isReady = !!status?.isReady;
|
|
1545
|
+
const isLocked = !!status?.isLocked;
|
|
1546
|
+
const isConfirmed = !!status?.isConfirmed;
|
|
1547
|
+
const composite = `${this.locale}:${this.namespace}:${key}`;
|
|
1548
|
+
const wrap = document.createElement("div");
|
|
1549
|
+
wrap.className = "lr-workflow-actions";
|
|
1550
|
+
const addBtn = (label, action, variant = "secondary") => {
|
|
1551
|
+
const btn = document.createElement("button");
|
|
1552
|
+
btn.className = `lr-wf-btn lr-wf-${variant}`;
|
|
1553
|
+
btn.textContent = label;
|
|
1554
|
+
btn.addEventListener("click", async () => {
|
|
1555
|
+
btn.disabled = true;
|
|
1556
|
+
const result = await this.api.transitionTarget(composite, action);
|
|
1557
|
+
btn.disabled = false;
|
|
1558
|
+
if (result) {
|
|
1559
|
+
await this.refreshStatusForKey(key);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
wrap.appendChild(btn);
|
|
1563
|
+
};
|
|
1564
|
+
if (!isLocked && !isReady) {
|
|
1565
|
+
addBtn("Confirm", "confirm", "primary");
|
|
1566
|
+
addBtn("Reject", "reject", "danger");
|
|
1567
|
+
}
|
|
1568
|
+
if (isConfirmed && !isLocked && !isReady) {
|
|
1569
|
+
addBtn("Unconfirm", "unconfirm");
|
|
1570
|
+
}
|
|
1571
|
+
if (isPM) {
|
|
1572
|
+
if (!isReady) {
|
|
1573
|
+
addBtn("Mark Ready", "mark-ready", "primary");
|
|
1574
|
+
} else {
|
|
1575
|
+
addBtn("Unmark Ready", "unmark-ready", "danger");
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (wrap.children.length > 0) {
|
|
1579
|
+
this.bodyEl.appendChild(wrap);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
/** Re-fetch status for one key and re-render the current view. */
|
|
1583
|
+
async refreshStatusForKey(key) {
|
|
1584
|
+
const fresh = await this.api.fetchStatus([key], this.locale, this.namespace);
|
|
1585
|
+
for (const [k, s] of Object.entries(fresh)) {
|
|
1586
|
+
this.statusMap.set(k, s);
|
|
1587
|
+
}
|
|
1588
|
+
if (this.current) {
|
|
1589
|
+
this.renderContent(this.current);
|
|
1590
|
+
}
|
|
1591
|
+
this.emitProgress();
|
|
1592
|
+
}
|
|
1404
1593
|
addField(label, contentFn) {
|
|
1405
1594
|
const field = document.createElement("div");
|
|
1406
1595
|
field.className = "lr-field";
|
|
@@ -1723,8 +1912,440 @@ var LionRapidEditor = (() => {
|
|
|
1723
1912
|
this.onSessionExpired();
|
|
1724
1913
|
}
|
|
1725
1914
|
}
|
|
1915
|
+
// ============================================
|
|
1916
|
+
// Snapshot API
|
|
1917
|
+
// ============================================
|
|
1918
|
+
/**
|
|
1919
|
+
* Upload a captured snapshot. Returns the created snapshot's UUID so the
|
|
1920
|
+
* caller can build a share URL (`?__lr_snapshot={uuid}`).
|
|
1921
|
+
*/
|
|
1922
|
+
async createSnapshot(snapshot) {
|
|
1923
|
+
try {
|
|
1924
|
+
const response = await fetch(`${this.apiBase}/api/editor/snapshots`, {
|
|
1925
|
+
method: "POST",
|
|
1926
|
+
headers: {
|
|
1927
|
+
"Content-Type": "application/json",
|
|
1928
|
+
"Authorization": `Bearer ${this.sessionToken}`
|
|
1929
|
+
},
|
|
1930
|
+
body: JSON.stringify({
|
|
1931
|
+
pageUrl: snapshot.pageUrl,
|
|
1932
|
+
title: snapshot.title,
|
|
1933
|
+
viewport: snapshot.viewport,
|
|
1934
|
+
textEntries: snapshot.textEntries,
|
|
1935
|
+
html: snapshot.html
|
|
1936
|
+
})
|
|
1937
|
+
});
|
|
1938
|
+
if (response.status === 401) {
|
|
1939
|
+
this.handleSessionExpired();
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
if (!response.ok) {
|
|
1943
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1944
|
+
}
|
|
1945
|
+
const data = await response.json();
|
|
1946
|
+
return { snapshotUuid: data.snapshot_uuid || data.snapshotUuid };
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
this.onStatus("error", `Snapshot failed: ${err.message}`);
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Re-capture an existing snapshot. Server appends a new version and keeps
|
|
1954
|
+
* the same `snapshotUuid`. Returns the new version number on success.
|
|
1955
|
+
*/
|
|
1956
|
+
async recaptureSnapshot(snapshotUuid, snapshot) {
|
|
1957
|
+
try {
|
|
1958
|
+
const response = await fetch(
|
|
1959
|
+
`${this.apiBase}/api/editor/snapshots/${encodeURIComponent(snapshotUuid)}`,
|
|
1960
|
+
{
|
|
1961
|
+
method: "PUT",
|
|
1962
|
+
headers: {
|
|
1963
|
+
"Content-Type": "application/json",
|
|
1964
|
+
"Authorization": `Bearer ${this.sessionToken}`
|
|
1965
|
+
},
|
|
1966
|
+
body: JSON.stringify({
|
|
1967
|
+
pageUrl: snapshot.pageUrl,
|
|
1968
|
+
title: snapshot.title,
|
|
1969
|
+
viewport: snapshot.viewport,
|
|
1970
|
+
textEntries: snapshot.textEntries,
|
|
1971
|
+
html: snapshot.html
|
|
1972
|
+
})
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
if (response.status === 401) {
|
|
1976
|
+
this.handleSessionExpired();
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
if (!response.ok) {
|
|
1980
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1981
|
+
}
|
|
1982
|
+
const data = await response.json();
|
|
1983
|
+
return {
|
|
1984
|
+
snapshotUuid: data.snapshot_uuid || data.snapshotUuid,
|
|
1985
|
+
version: data.version || 1
|
|
1986
|
+
};
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
this.onStatus("error", `Re-capture failed: ${err.message}`);
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Fetch a single snapshot by UUID. Returns null on auth failure or 404.
|
|
1994
|
+
*/
|
|
1995
|
+
async getSnapshot(snapshotUuid) {
|
|
1996
|
+
try {
|
|
1997
|
+
const response = await fetch(
|
|
1998
|
+
`${this.apiBase}/api/editor/snapshots/${encodeURIComponent(snapshotUuid)}`,
|
|
1999
|
+
{
|
|
2000
|
+
headers: { "Authorization": `Bearer ${this.sessionToken}` }
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
if (response.status === 401) {
|
|
2004
|
+
this.handleSessionExpired();
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
if (!response.ok) return null;
|
|
2008
|
+
return await response.json();
|
|
2009
|
+
} catch {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* List all snapshots the current editor session can see.
|
|
2015
|
+
*/
|
|
2016
|
+
async listSnapshots() {
|
|
2017
|
+
try {
|
|
2018
|
+
const response = await fetch(`${this.apiBase}/api/editor/snapshots`, {
|
|
2019
|
+
headers: { "Authorization": `Bearer ${this.sessionToken}` }
|
|
2020
|
+
});
|
|
2021
|
+
if (response.status === 401) {
|
|
2022
|
+
this.handleSessionExpired();
|
|
2023
|
+
return [];
|
|
2024
|
+
}
|
|
2025
|
+
if (!response.ok) return [];
|
|
2026
|
+
const data = await response.json();
|
|
2027
|
+
return data.snapshots || [];
|
|
2028
|
+
} catch {
|
|
2029
|
+
return [];
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Delete (soft) a snapshot by UUID.
|
|
2034
|
+
*/
|
|
2035
|
+
async deleteSnapshot(snapshotUuid) {
|
|
2036
|
+
try {
|
|
2037
|
+
const response = await fetch(
|
|
2038
|
+
`${this.apiBase}/api/editor/snapshots/${encodeURIComponent(snapshotUuid)}`,
|
|
2039
|
+
{
|
|
2040
|
+
method: "DELETE",
|
|
2041
|
+
headers: { "Authorization": `Bearer ${this.sessionToken}` }
|
|
2042
|
+
}
|
|
2043
|
+
);
|
|
2044
|
+
if (response.status === 401) {
|
|
2045
|
+
this.handleSessionExpired();
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
return response.ok;
|
|
2049
|
+
} catch {
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Workflow transitions (Phase 3). Per-key endpoints on the integrations API
|
|
2055
|
+
* that mirror the dashboard's row-level transitions but are addressable by
|
|
2056
|
+
* composite editor key (locale:namespace:auto_xxx).
|
|
2057
|
+
*
|
|
2058
|
+
* Returns the parsed JSON on success, or null on failure (caller usually
|
|
2059
|
+
* shows a toast / re-fetches status).
|
|
2060
|
+
*/
|
|
2061
|
+
async transitionTarget(composite, action) {
|
|
2062
|
+
try {
|
|
2063
|
+
const response = await fetch(
|
|
2064
|
+
`${this.apiBase}/api/integrations/targets/${encodeURIComponent(composite)}/${action}`,
|
|
2065
|
+
{
|
|
2066
|
+
method: "POST",
|
|
2067
|
+
headers: { "Authorization": `Bearer ${this.sessionToken}` }
|
|
2068
|
+
}
|
|
2069
|
+
);
|
|
2070
|
+
if (response.status === 401) {
|
|
2071
|
+
this.handleSessionExpired();
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
if (!response.ok) {
|
|
2075
|
+
this.onStatus("error", `${action} failed: HTTP ${response.status}`);
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
return await response.json();
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
this.onStatus("error", `${action} failed: ${err.message}`);
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
1726
2084
|
};
|
|
1727
2085
|
|
|
2086
|
+
// src/browser/editor/pii-redactor.ts
|
|
2087
|
+
var REDACTED = "[REDACTED]";
|
|
2088
|
+
var EMAIL_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi;
|
|
2089
|
+
var PHONE_RE = /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}/g;
|
|
2090
|
+
var SSN_RE = /\b\d{3}[\s-]\d{2}[\s-]\d{4}\b/g;
|
|
2091
|
+
var IPV4_RE = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g;
|
|
2092
|
+
var URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"']+/gi;
|
|
2093
|
+
var CC_CANDIDATE_RE = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
2094
|
+
function luhnValid(digits) {
|
|
2095
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
2096
|
+
let sum = 0;
|
|
2097
|
+
let alt = false;
|
|
2098
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
2099
|
+
let n = digits.charCodeAt(i) - 48;
|
|
2100
|
+
if (n < 0 || n > 9) return false;
|
|
2101
|
+
if (alt) {
|
|
2102
|
+
n *= 2;
|
|
2103
|
+
if (n > 9) n -= 9;
|
|
2104
|
+
}
|
|
2105
|
+
sum += n;
|
|
2106
|
+
alt = !alt;
|
|
2107
|
+
}
|
|
2108
|
+
return sum % 10 === 0;
|
|
2109
|
+
}
|
|
2110
|
+
function ipv4Valid(match) {
|
|
2111
|
+
const parts = match.split(".");
|
|
2112
|
+
if (parts.length !== 4) return false;
|
|
2113
|
+
for (const p of parts) {
|
|
2114
|
+
const n = Number(p);
|
|
2115
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) return false;
|
|
2116
|
+
}
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
function redactString(text) {
|
|
2120
|
+
if (!text) return text;
|
|
2121
|
+
let out = text;
|
|
2122
|
+
out = out.replace(URL_RE, REDACTED);
|
|
2123
|
+
out = out.replace(EMAIL_RE, REDACTED);
|
|
2124
|
+
out = out.replace(PHONE_RE, REDACTED);
|
|
2125
|
+
out = out.replace(SSN_RE, REDACTED);
|
|
2126
|
+
out = out.replace(IPV4_RE, (m) => ipv4Valid(m) ? REDACTED : m);
|
|
2127
|
+
out = out.replace(CC_CANDIDATE_RE, (m) => {
|
|
2128
|
+
const digits = m.replace(/[\s-]/g, "");
|
|
2129
|
+
return luhnValid(digits) ? REDACTED : m;
|
|
2130
|
+
});
|
|
2131
|
+
return out;
|
|
2132
|
+
}
|
|
2133
|
+
var SKIP_TAGS3 = /* @__PURE__ */ new Set(["CODE", "PRE", "STYLE", "TEXTAREA"]);
|
|
2134
|
+
function redactTextNodes(root) {
|
|
2135
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
2136
|
+
const toUpdate = [];
|
|
2137
|
+
let node = walker.nextNode();
|
|
2138
|
+
while (node) {
|
|
2139
|
+
if (!isInSkippedSubtree(node)) {
|
|
2140
|
+
const original = node.nodeValue || "";
|
|
2141
|
+
const replaced = redactString(original);
|
|
2142
|
+
if (replaced !== original) {
|
|
2143
|
+
toUpdate.push({ node, value: replaced });
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
node = walker.nextNode();
|
|
2147
|
+
}
|
|
2148
|
+
for (const { node: textNode, value } of toUpdate) {
|
|
2149
|
+
textNode.nodeValue = value;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
function isInSkippedSubtree(node) {
|
|
2153
|
+
let cur = node.parentNode;
|
|
2154
|
+
while (cur && cur.nodeType === Node.ELEMENT_NODE) {
|
|
2155
|
+
const el = cur;
|
|
2156
|
+
if (SKIP_TAGS3.has(el.tagName)) return true;
|
|
2157
|
+
if (el.hasAttribute("data-no-redact")) return true;
|
|
2158
|
+
cur = el.parentNode;
|
|
2159
|
+
}
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// src/browser/editor/snapshot-capture.ts
|
|
2164
|
+
var REDACTED2 = "[REDACTED]";
|
|
2165
|
+
function captureSnapshot() {
|
|
2166
|
+
const clone = document.documentElement.cloneNode(true);
|
|
2167
|
+
const textEntries = extractTextEntries(clone);
|
|
2168
|
+
removeDangerousElements(clone);
|
|
2169
|
+
clearFormValues(clone);
|
|
2170
|
+
redactSensitiveElements(clone);
|
|
2171
|
+
stripInlineEventHandlers(clone);
|
|
2172
|
+
redactTextNodePII(clone);
|
|
2173
|
+
stripSensitiveMetaAndHeaders(clone);
|
|
2174
|
+
const html = "<!DOCTYPE html>\n" + clone.outerHTML;
|
|
2175
|
+
return {
|
|
2176
|
+
html,
|
|
2177
|
+
textEntries,
|
|
2178
|
+
pageUrl: window.location.href,
|
|
2179
|
+
title: document.title,
|
|
2180
|
+
viewport: {
|
|
2181
|
+
width: window.innerWidth,
|
|
2182
|
+
height: window.innerHeight,
|
|
2183
|
+
devicePixelRatio: window.devicePixelRatio || 1
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
function removeDangerousElements(root) {
|
|
2188
|
+
const selectors = [
|
|
2189
|
+
"script",
|
|
2190
|
+
"noscript",
|
|
2191
|
+
"iframe",
|
|
2192
|
+
"embed",
|
|
2193
|
+
"object",
|
|
2194
|
+
"#__lionrapid-editor",
|
|
2195
|
+
'link[rel="prefetch"]',
|
|
2196
|
+
'link[rel="preload"]',
|
|
2197
|
+
'link[rel="modulepreload"]'
|
|
2198
|
+
];
|
|
2199
|
+
for (const selector of selectors) {
|
|
2200
|
+
const matches = root.querySelectorAll(selector);
|
|
2201
|
+
for (let i = 0; i < matches.length; i++) {
|
|
2202
|
+
matches[i].remove();
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function clearFormValues(root) {
|
|
2207
|
+
const inputs = root.querySelectorAll("input, textarea, select");
|
|
2208
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
2209
|
+
const el = inputs[i];
|
|
2210
|
+
if ("value" in el) el.value = "";
|
|
2211
|
+
if (el.hasAttribute("value")) el.setAttribute("value", "");
|
|
2212
|
+
if (el.tagName === "TEXTAREA") el.textContent = "";
|
|
2213
|
+
if (el.tagName === "SELECT") {
|
|
2214
|
+
const options = el.querySelectorAll("option");
|
|
2215
|
+
for (let j = 0; j < options.length; j++) {
|
|
2216
|
+
options[j].selected = false;
|
|
2217
|
+
options[j].removeAttribute("selected");
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const attrs = Array.from(el.attributes);
|
|
2221
|
+
for (const attr of attrs) {
|
|
2222
|
+
if (attr.name.startsWith("data-value") || attr.name === "data-initial") {
|
|
2223
|
+
el.removeAttribute(attr.name);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
function redactSensitiveElements(root) {
|
|
2229
|
+
const sensitive = root.querySelectorAll("[data-pii], [data-sensitive]");
|
|
2230
|
+
for (let i = 0; i < sensitive.length; i++) {
|
|
2231
|
+
const el = sensitive[i];
|
|
2232
|
+
el.textContent = REDACTED2;
|
|
2233
|
+
const attrs = Array.from(el.attributes);
|
|
2234
|
+
for (const attr of attrs) {
|
|
2235
|
+
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
|
|
2236
|
+
el.removeAttribute(attr.name);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function stripInlineEventHandlers(root) {
|
|
2242
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
2243
|
+
let node = walker.nextNode();
|
|
2244
|
+
while (node) {
|
|
2245
|
+
const attrs = Array.from(node.attributes);
|
|
2246
|
+
for (const attr of attrs) {
|
|
2247
|
+
if (attr.name.toLowerCase().startsWith("on")) {
|
|
2248
|
+
node.removeAttribute(attr.name);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
node = walker.nextNode();
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function redactTextNodePII(root) {
|
|
2255
|
+
redactTextNodes(root);
|
|
2256
|
+
}
|
|
2257
|
+
function stripSensitiveMetaAndHeaders(root) {
|
|
2258
|
+
const sensitiveMetas = root.querySelectorAll(
|
|
2259
|
+
'meta[name="csrf-token"], meta[name="csp-nonce"], meta[name="csrf-param"], meta[http-equiv="set-cookie"]'
|
|
2260
|
+
);
|
|
2261
|
+
for (let i = 0; i < sensitiveMetas.length; i++) {
|
|
2262
|
+
sensitiveMetas[i].remove();
|
|
2263
|
+
}
|
|
2264
|
+
const TOKEN_PARAMS = ["token", "auth", "access_token", "session", "sid", "key", "api_key", "apikey"];
|
|
2265
|
+
const urlElements = root.querySelectorAll("link[href], a[href], img[src]");
|
|
2266
|
+
for (let i = 0; i < urlElements.length; i++) {
|
|
2267
|
+
const el = urlElements[i];
|
|
2268
|
+
const attr = el.tagName === "IMG" ? "src" : "href";
|
|
2269
|
+
const raw = el.getAttribute(attr);
|
|
2270
|
+
if (!raw) continue;
|
|
2271
|
+
try {
|
|
2272
|
+
const url = new URL(raw, window.location.href);
|
|
2273
|
+
let changed = false;
|
|
2274
|
+
for (const param of TOKEN_PARAMS) {
|
|
2275
|
+
if (url.searchParams.has(param)) {
|
|
2276
|
+
url.searchParams.set(param, REDACTED2);
|
|
2277
|
+
changed = true;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (changed) el.setAttribute(attr, url.toString());
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function extractTextEntries(clone) {
|
|
2286
|
+
const extractor = new BlockExtractor(
|
|
2287
|
+
{ emails: true, urls: true, numbers: true, phones: true },
|
|
2288
|
+
[],
|
|
2289
|
+
2
|
|
2290
|
+
);
|
|
2291
|
+
const blocks = extractor.extract(clone);
|
|
2292
|
+
return blocks.map((block, index) => {
|
|
2293
|
+
block.element.setAttribute("data-lr-snap-idx", String(index));
|
|
2294
|
+
return {
|
|
2295
|
+
key: block.key,
|
|
2296
|
+
tagName: block.element.tagName,
|
|
2297
|
+
blockIndex: index
|
|
2298
|
+
};
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/browser/editor/snapshot-viewer.ts
|
|
2303
|
+
async function activateSnapshotViewer(snapshotUuid, api) {
|
|
2304
|
+
const snapshot = await api.getSnapshot(snapshotUuid);
|
|
2305
|
+
if (!snapshot) return null;
|
|
2306
|
+
const parser = new DOMParser();
|
|
2307
|
+
const doc = parser.parseFromString(snapshot.html, "text/html");
|
|
2308
|
+
const snapshotStyles = doc.head.querySelectorAll('link[rel="stylesheet"], style');
|
|
2309
|
+
for (let i = 0; i < snapshotStyles.length; i++) {
|
|
2310
|
+
const clone = snapshotStyles[i].cloneNode(true);
|
|
2311
|
+
clone.setAttribute("data-lr-snapshot-style", "");
|
|
2312
|
+
document.head.appendChild(clone);
|
|
2313
|
+
}
|
|
2314
|
+
if (snapshot.title) {
|
|
2315
|
+
document.title = `[Snapshot] ${snapshot.title}`;
|
|
2316
|
+
}
|
|
2317
|
+
const editorHost = document.getElementById("__lionrapid-editor");
|
|
2318
|
+
while (document.body.firstChild) {
|
|
2319
|
+
if (document.body.firstChild === editorHost) {
|
|
2320
|
+
const next = document.body.firstChild.nextSibling;
|
|
2321
|
+
if (!next) break;
|
|
2322
|
+
document.body.removeChild(next);
|
|
2323
|
+
} else {
|
|
2324
|
+
document.body.removeChild(document.body.firstChild);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
const root = document.createElement("div");
|
|
2328
|
+
root.id = "__lr_snapshot_root";
|
|
2329
|
+
root.setAttribute("data-lr-snapshot-uuid", snapshotUuid);
|
|
2330
|
+
root.innerHTML = doc.body.innerHTML;
|
|
2331
|
+
if (editorHost && editorHost.parentNode === document.body) {
|
|
2332
|
+
document.body.insertBefore(root, editorHost);
|
|
2333
|
+
} else {
|
|
2334
|
+
document.body.appendChild(root);
|
|
2335
|
+
}
|
|
2336
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
2337
|
+
const entries = snapshot.textEntries || [];
|
|
2338
|
+
for (const entry of entries) {
|
|
2339
|
+
keyMap.set(entry.blockIndex, entry.key);
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
root,
|
|
2343
|
+
pageUrl: snapshot.pageUrl,
|
|
2344
|
+
title: snapshot.title,
|
|
2345
|
+
keyMap
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
|
|
1728
2349
|
// src/browser/editor/editor-init.ts
|
|
1729
2350
|
var SESSION_KEY = "__lr_editor_session";
|
|
1730
2351
|
function getStoredSession() {
|
|
@@ -1755,6 +2376,17 @@ var LionRapidEditor = (() => {
|
|
|
1755
2376
|
function clearSession() {
|
|
1756
2377
|
sessionStorage.removeItem(SESSION_KEY);
|
|
1757
2378
|
}
|
|
2379
|
+
function extractSessionRole(token) {
|
|
2380
|
+
try {
|
|
2381
|
+
const raw = token.startsWith("es_") ? token.slice(3) : token;
|
|
2382
|
+
const parts = raw.split(".");
|
|
2383
|
+
if (parts.length !== 3) return "";
|
|
2384
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
2385
|
+
return typeof payload.role === "string" ? payload.role : "";
|
|
2386
|
+
} catch {
|
|
2387
|
+
return "";
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
1758
2390
|
function showEditorNotice(message) {
|
|
1759
2391
|
const el = document.createElement("div");
|
|
1760
2392
|
el.setAttribute("data-no-translate", "");
|
|
@@ -1791,12 +2423,15 @@ var LionRapidEditor = (() => {
|
|
|
1791
2423
|
setTimeout(() => el.remove(), 8e3);
|
|
1792
2424
|
}
|
|
1793
2425
|
function initEditor() {
|
|
1794
|
-
const scriptEl = document.querySelector("script[data-grant]");
|
|
2426
|
+
const scriptEl = document.querySelector("script[data-grant]") || document.querySelector("script[data-lr-editor]");
|
|
1795
2427
|
if (!scriptEl) {
|
|
1796
|
-
console.warn("[LionRapid Editor] No script tag with data-grant found");
|
|
1797
2428
|
return;
|
|
1798
2429
|
}
|
|
1799
2430
|
const grant = scriptEl.dataset.grant || "";
|
|
2431
|
+
const hasValidSession = getStoredSession() !== null;
|
|
2432
|
+
if (!grant && !hasValidSession) {
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
1800
2435
|
const locale = scriptEl.dataset.locale || "";
|
|
1801
2436
|
const apiBase = scriptEl.dataset.api || "https://api.lionrapid.com";
|
|
1802
2437
|
const projectId = scriptEl.dataset.project || "";
|
|
@@ -1850,6 +2485,8 @@ var LionRapidEditor = (() => {
|
|
|
1850
2485
|
showEditorNotice("Editor session expired. Please reopen the editor from the dashboard.");
|
|
1851
2486
|
return;
|
|
1852
2487
|
}
|
|
2488
|
+
const role = extractSessionRole(sessionToken);
|
|
2489
|
+
const canCaptureSnapshots = role === "project_manager" || role === "admin";
|
|
1853
2490
|
const host = document.createElement("div");
|
|
1854
2491
|
host.id = "__lionrapid-editor";
|
|
1855
2492
|
host.setAttribute("data-no-translate", "");
|
|
@@ -1862,12 +2499,26 @@ var LionRapidEditor = (() => {
|
|
|
1862
2499
|
if (switcherEl) switcherEl.style.display = "none";
|
|
1863
2500
|
const api = new EditorAPI(normalizedApiBase, sessionToken, projectId);
|
|
1864
2501
|
api.startAutoFlush();
|
|
2502
|
+
const currentUrl = new URL(window.location.href);
|
|
2503
|
+
const snapshotUuid = currentUrl.searchParams.get("__lr_snapshot");
|
|
2504
|
+
const recaptureUuid = currentUrl.searchParams.get("__lr_recapture");
|
|
2505
|
+
let inspectorRoot;
|
|
2506
|
+
let snapshotKeyMap;
|
|
2507
|
+
if (snapshotUuid) {
|
|
2508
|
+
const result = await activateSnapshotViewer(snapshotUuid, api);
|
|
2509
|
+
if (!result) {
|
|
2510
|
+
showEditorNotice("Snapshot not found or expired. Please check the dashboard.");
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
inspectorRoot = result.root;
|
|
2514
|
+
snapshotKeyMap = result.keyMap;
|
|
2515
|
+
}
|
|
1865
2516
|
const handleBlockSelect = (resolved) => {
|
|
1866
2517
|
sidebar.open(resolved);
|
|
1867
2518
|
inspector.clearHighlight();
|
|
1868
2519
|
};
|
|
1869
|
-
const sidebar = new Sidebar(shadow, api, currentLocale, namespace, defaultLocale, handleBlockSelect);
|
|
1870
|
-
const inspector = new Inspector(shadow, handleBlockSelect);
|
|
2520
|
+
const sidebar = new Sidebar(shadow, api, currentLocale, namespace, defaultLocale, handleBlockSelect, role);
|
|
2521
|
+
const inspector = new Inspector(shadow, handleBlockSelect, inspectorRoot, snapshotKeyMap);
|
|
1871
2522
|
const toolbar = new Toolbar(
|
|
1872
2523
|
shadow,
|
|
1873
2524
|
languages,
|
|
@@ -1898,7 +2549,43 @@ var LionRapidEditor = (() => {
|
|
|
1898
2549
|
sidebar.destroy();
|
|
1899
2550
|
host.remove();
|
|
1900
2551
|
clearSession();
|
|
1901
|
-
}
|
|
2552
|
+
},
|
|
2553
|
+
// Capture Snapshot — only available in live-page mode (not viewer) AND
|
|
2554
|
+
// only for PM/admin roles. Translators and proofreaders cannot capture;
|
|
2555
|
+
// the server enforces the same check on POST /api/editor/snapshots.
|
|
2556
|
+
//
|
|
2557
|
+
// If ?__lr_recapture={uuid} is present, the same button re-captures that
|
|
2558
|
+
// existing snapshot (PUT) instead of creating a new one (POST).
|
|
2559
|
+
snapshotUuid || !canCaptureSnapshots ? null : async () => {
|
|
2560
|
+
try {
|
|
2561
|
+
const snapshot = captureSnapshot();
|
|
2562
|
+
if (recaptureUuid) {
|
|
2563
|
+
const result = await api.recaptureSnapshot(recaptureUuid, snapshot);
|
|
2564
|
+
if (result) {
|
|
2565
|
+
showEditorNotice(
|
|
2566
|
+
`Re-captured as v${result.version} (${snapshot.textEntries.length} blocks). You can close this tab.`
|
|
2567
|
+
);
|
|
2568
|
+
} else {
|
|
2569
|
+
showEditorNotice("Re-capture failed. Please try again.");
|
|
2570
|
+
}
|
|
2571
|
+
} else {
|
|
2572
|
+
const result = await api.createSnapshot(snapshot);
|
|
2573
|
+
if (result) {
|
|
2574
|
+
showEditorNotice(
|
|
2575
|
+
`Snapshot captured (${snapshot.textEntries.length} blocks). Open it from the Snapshots tab in the dashboard.`
|
|
2576
|
+
);
|
|
2577
|
+
} else {
|
|
2578
|
+
showEditorNotice("Snapshot upload failed. Please try again.");
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
console.error("[LionRapid Editor] Snapshot capture error:", err);
|
|
2583
|
+
showEditorNotice("Snapshot capture failed. See console for details.");
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
// When in recapture mode, surface it to the toolbar so the button label
|
|
2587
|
+
// can change from "Capture" to "Save re-capture".
|
|
2588
|
+
recaptureUuid ? { mode: "recapture" } : void 0
|
|
1902
2589
|
);
|
|
1903
2590
|
sidebar.setProgressCallback((translated, total) => {
|
|
1904
2591
|
toolbar.updateProgress(translated, total);
|
|
@@ -1907,8 +2594,20 @@ var LionRapidEditor = (() => {
|
|
|
1907
2594
|
toolbar.updateProgress(sidebar.getTranslatedCount(), sidebar.getTotalCount());
|
|
1908
2595
|
});
|
|
1909
2596
|
const url = new URL(window.location.href);
|
|
2597
|
+
let dirty = false;
|
|
1910
2598
|
if (url.searchParams.has("__lr_editor")) {
|
|
1911
2599
|
url.searchParams.delete("__lr_editor");
|
|
2600
|
+
dirty = true;
|
|
2601
|
+
}
|
|
2602
|
+
if (url.searchParams.has("__lr_snapshot")) {
|
|
2603
|
+
url.searchParams.delete("__lr_snapshot");
|
|
2604
|
+
dirty = true;
|
|
2605
|
+
}
|
|
2606
|
+
if (url.searchParams.has("__lr_recapture")) {
|
|
2607
|
+
url.searchParams.delete("__lr_recapture");
|
|
2608
|
+
dirty = true;
|
|
2609
|
+
}
|
|
2610
|
+
if (dirty) {
|
|
1912
2611
|
history.replaceState(null, "", url.toString());
|
|
1913
2612
|
}
|
|
1914
2613
|
}
|