@replohq/sandbox-runtime 1.24.0 → 1.24.2
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/_vendor/schemas/placeholderProductsProtocol.d.ts +1 -0
- package/_vendor/schemas/placeholderProductsProtocol.mjs +2 -1
- package/buildMetadata.js +3 -3
- package/comments-overlay/bridge.js +22 -17
- package/comments-overlay/overlay.d.ts +1 -1
- package/comments-overlay/overlay.js +52 -44
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@ export declare const PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID: "replo:placehol
|
|
|
2
2
|
export declare const PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION: 3;
|
|
3
3
|
export declare const PLACEHOLDER_HERO_PRODUCT_ID = "f0921037-e530-4e67-a612-d573b7c40b19";
|
|
4
4
|
export declare const PLACEHOLDER_PRODUCT_ID = "c53f98cd-0989-4661-a7c2-a0304d184676";
|
|
5
|
+
export declare const PLACEHOLDER_PRODUCT_LIMIT = 12;
|
|
5
6
|
export type PlaceholderProductId = typeof PLACEHOLDER_HERO_PRODUCT_ID | typeof PLACEHOLDER_PRODUCT_ID;
|
|
6
7
|
export declare const PLACEHOLDER_PRODUCTS_SOURCE_KEY = "__REPLO_PLACEHOLDER_PRODUCTS__";
|
|
7
8
|
export declare const PLACEHOLDER_PRODUCTS_CHANGE_EVENT = "replo:placeholder-products";
|
|
@@ -2,7 +2,8 @@ var PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID = "replo:placeholder-products-ifr
|
|
|
2
2
|
var PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION = 3;
|
|
3
3
|
var PLACEHOLDER_HERO_PRODUCT_ID = "f0921037-e530-4e67-a612-d573b7c40b19";
|
|
4
4
|
var PLACEHOLDER_PRODUCT_ID = "c53f98cd-0989-4661-a7c2-a0304d184676";
|
|
5
|
+
var PLACEHOLDER_PRODUCT_LIMIT = 12;
|
|
5
6
|
var PLACEHOLDER_PRODUCTS_SOURCE_KEY = "__REPLO_PLACEHOLDER_PRODUCTS__";
|
|
6
7
|
var PLACEHOLDER_PRODUCTS_CHANGE_EVENT = "replo:placeholder-products";
|
|
7
8
|
|
|
8
|
-
export { PLACEHOLDER_HERO_PRODUCT_ID, PLACEHOLDER_PRODUCTS_CHANGE_EVENT, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION, PLACEHOLDER_PRODUCTS_SOURCE_KEY, PLACEHOLDER_PRODUCT_ID };
|
|
9
|
+
export { PLACEHOLDER_HERO_PRODUCT_ID, PLACEHOLDER_PRODUCTS_CHANGE_EVENT, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION, PLACEHOLDER_PRODUCTS_SOURCE_KEY, PLACEHOLDER_PRODUCT_ID, PLACEHOLDER_PRODUCT_LIMIT };
|
package/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.2",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-11-
|
|
5
|
+
"commit": "a3a1a0505f42cea89e63bbfa5628b1d35558fc76",
|
|
6
|
+
"builtAt": "2026-11-03T19:15:42.310Z"
|
|
7
7
|
};
|
|
@@ -55,20 +55,20 @@ function startCommentsOverlay({
|
|
|
55
55
|
handlers: {
|
|
56
56
|
setCommentMode: ({ mode }) => {
|
|
57
57
|
announceToVerifiedHost();
|
|
58
|
-
|
|
58
|
+
getOverlay().setMode(mode);
|
|
59
59
|
},
|
|
60
60
|
setPins: ({ pins }) => {
|
|
61
61
|
announceToVerifiedHost();
|
|
62
|
-
|
|
62
|
+
getOverlay().setPins(pins);
|
|
63
63
|
},
|
|
64
64
|
focusThread: ({ threadId }) => {
|
|
65
|
-
|
|
65
|
+
getOverlay().focusThread(threadId);
|
|
66
66
|
},
|
|
67
67
|
clearDraft: () => {
|
|
68
68
|
overlay?.clearDraft();
|
|
69
69
|
},
|
|
70
70
|
setDraftAnchor: ({ anchor }) => {
|
|
71
|
-
|
|
71
|
+
getOverlay().setDraftAnchor(anchor);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
});
|
|
@@ -101,19 +101,24 @@ function startCommentsOverlay({
|
|
|
101
101
|
() => reportLocation({ force: false }),
|
|
102
102
|
LOCATION_POLL_MS
|
|
103
103
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
function getOverlay() {
|
|
105
|
+
overlay ??= createCommentsOverlay({
|
|
106
|
+
prepareAnchor: (anchor) => symbolicateAnchor(anchor, symbolicate),
|
|
107
|
+
getSource: createSymbolicatedSourceLookup({
|
|
108
|
+
symbolicate,
|
|
109
|
+
onUpdate: () => overlay?.refresh()
|
|
110
|
+
}),
|
|
111
|
+
onAnchorPicked: (input) => host.anchorPicked(input),
|
|
112
|
+
onPinClicked: (input) => host.pinClicked(input),
|
|
113
|
+
onAnchorStatuses: (input) => host.anchorStatuses(input),
|
|
114
|
+
onComposeFromSelection: (input) => {
|
|
115
|
+
return host.requestComposeFromSelection(input);
|
|
116
|
+
},
|
|
117
|
+
onPickCancelled: () => host.pickCancelled(),
|
|
118
|
+
onModeShortcut: (input) => host.modeShortcut(input)
|
|
119
|
+
});
|
|
120
|
+
return overlay;
|
|
121
|
+
}
|
|
117
122
|
const unsubscribe = channel.subscribe();
|
|
118
123
|
isAnnouncing = true;
|
|
119
124
|
try {
|
|
@@ -3,7 +3,7 @@ import type { CommentOverlayAnchorStatus, CommentOverlayPin, CommentOverlayRect
|
|
|
3
3
|
import type { DisplayMode } from "../_vendor/schemas/generated/selectMode";
|
|
4
4
|
import type { ElementSource } from "./capture";
|
|
5
5
|
export declare const OVERLAY_Z_INDEX = 2147483645;
|
|
6
|
-
export declare const HIGHLIGHT_BACKGROUND = "
|
|
6
|
+
export declare const HIGHLIGHT_BACKGROUND = "color-mix(in srgb, var(--replo-comment-accent) 12%, transparent)";
|
|
7
7
|
export declare const BADGE_SIZE_PX = 32;
|
|
8
8
|
export declare const RERESOLVE_DEBOUNCE_MS = 250;
|
|
9
9
|
export type CommentsOverlayCallbacks = {
|
|
@@ -2,8 +2,9 @@ import { captureAnchor, getAnchorElement } from "./capture";
|
|
|
2
2
|
import { isResolutionVisible, resolveAnchor } from "./resolve";
|
|
3
3
|
import { OVERLAY_ROOT_ATTRIBUTE } from "./textQuote";
|
|
4
4
|
const OVERLAY_Z_INDEX = 2147483645;
|
|
5
|
-
const HIGHLIGHT_BACKGROUND = "
|
|
6
|
-
const
|
|
5
|
+
const HIGHLIGHT_BACKGROUND = "color-mix(in srgb, var(--replo-comment-accent) 12%, transparent)";
|
|
6
|
+
const SOFT_OUTLINE_COLOR = "color-mix(in srgb, var(--replo-comment-accent) 55%, transparent)";
|
|
7
|
+
const HIGHLIGHT_OUTLINE = "1.5px solid var(--replo-comment-accent)";
|
|
7
8
|
const BADGE_SIZE_PX = 32;
|
|
8
9
|
const BADGE_HIT_TARGET_PX = 44;
|
|
9
10
|
const BADGE_HIT_TARGET_WIDTH_PX = 64;
|
|
@@ -11,7 +12,7 @@ const COMPACT_GUTTER_MAX_WIDTH_PX = 600;
|
|
|
11
12
|
const RERESOLVE_DEBOUNCE_MS = 250;
|
|
12
13
|
const SELECTION_PROMPT_DEBOUNCE_MS = 150;
|
|
13
14
|
const TEXT_PICK_CLICK_SUPPRESS_MS = 750;
|
|
14
|
-
const HOVER_OUTLINE_STYLE =
|
|
15
|
+
const HOVER_OUTLINE_STYLE = `position:absolute;pointer-events:none;box-sizing:border-box;border:${HIGHLIGHT_OUTLINE};background:transparent;border-radius:4px`;
|
|
15
16
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
16
17
|
const EDITABLE_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false'])";
|
|
17
18
|
function getReportedStatus(resolution) {
|
|
@@ -31,11 +32,8 @@ function createCommentsOverlay(options) {
|
|
|
31
32
|
let pins = [];
|
|
32
33
|
let resolutions = /* @__PURE__ */ new Map();
|
|
33
34
|
const boundElements = /* @__PURE__ */ new Map();
|
|
34
|
-
let
|
|
35
|
+
let selection = null;
|
|
35
36
|
let hoveredBadgeThreadId = null;
|
|
36
|
-
let draftAnchor = null;
|
|
37
|
-
let draftResolution = null;
|
|
38
|
-
let draftElement = null;
|
|
39
37
|
let statusesKey = "";
|
|
40
38
|
let hoveredElement = null;
|
|
41
39
|
let hoverOutline = null;
|
|
@@ -52,7 +50,7 @@ function createCommentsOverlay(options) {
|
|
|
52
50
|
let isDestroyed = false;
|
|
53
51
|
const root = document.createElement("div");
|
|
54
52
|
root.setAttribute(OVERLAY_ROOT_ATTRIBUTE, "true");
|
|
55
|
-
root.style.cssText = `position:fixed;inset:0;z-index:${OVERLAY_Z_INDEX};pointer-events:none;overflow:hidden`;
|
|
53
|
+
root.style.cssText = `position:fixed;inset:0;z-index:${OVERLAY_Z_INDEX};pointer-events:none;overflow:hidden;--replo-comment-accent:light-dark(#7861bf,#b09be8);--replo-comment-surface:light-dark(#f0eafa,#372e4d);--replo-comment-text:light-dark(#7058b4,#b09be8);--replo-comment-on-accent:light-dark(#fff,#20182f)`;
|
|
56
54
|
document.documentElement.append(root);
|
|
57
55
|
const isInOverlay = (node) => {
|
|
58
56
|
return node instanceof Node && root.contains(node);
|
|
@@ -66,22 +64,22 @@ function createCommentsOverlay(options) {
|
|
|
66
64
|
const isPageElement = (node) => {
|
|
67
65
|
return node instanceof Element && node !== document.body && node !== document.documentElement && !isInOverlay(node);
|
|
68
66
|
};
|
|
69
|
-
const isEditableSelection = (
|
|
70
|
-
const node =
|
|
67
|
+
const isEditableSelection = (selection2) => {
|
|
68
|
+
const node = selection2.anchorNode;
|
|
71
69
|
const element = node instanceof Element ? node : node?.parentElement;
|
|
72
70
|
return Boolean(
|
|
73
71
|
element?.closest(EDITABLE_SELECTOR) || document.activeElement?.matches(EDITABLE_SELECTOR)
|
|
74
72
|
);
|
|
75
73
|
};
|
|
76
74
|
const getUsableSelectionRange = () => {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
75
|
+
const selection2 = window.getSelection();
|
|
76
|
+
if (!selection2 || selection2.isCollapsed || selection2.rangeCount === 0) {
|
|
79
77
|
return null;
|
|
80
78
|
}
|
|
81
|
-
if (isEditableSelection(
|
|
79
|
+
if (isEditableSelection(selection2)) {
|
|
82
80
|
return null;
|
|
83
81
|
}
|
|
84
|
-
const range =
|
|
82
|
+
const range = selection2.getRangeAt(0);
|
|
85
83
|
if (!range.toString().trim() || isInOverlay(range.commonAncestorContainer)) {
|
|
86
84
|
return null;
|
|
87
85
|
}
|
|
@@ -132,11 +130,12 @@ function createCommentsOverlay(options) {
|
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
resolutions = next;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
if (selection?.kind === "draft") {
|
|
134
|
+
selection.resolution = resolveAnchor(selection.anchor, document, {
|
|
135
|
+
previous: selection.resolution?.element ?? null,
|
|
136
|
+
getSource: cachedGetSource
|
|
137
|
+
});
|
|
138
|
+
}
|
|
140
139
|
const statuses = getElementAnchors().map(({ pin }) => {
|
|
141
140
|
return {
|
|
142
141
|
threadId: pin.threadId,
|
|
@@ -157,7 +156,7 @@ function createCommentsOverlay(options) {
|
|
|
157
156
|
resolveTimer = setTimeout(resolveAll, RERESOLVE_DEBOUNCE_MS);
|
|
158
157
|
};
|
|
159
158
|
const paintHighlight = (resolution, threadId) => {
|
|
160
|
-
const
|
|
159
|
+
const isOutlined = threadId === null || threadId === hoveredBadgeThreadId || selection?.kind === "thread" && threadId === selection.threadId;
|
|
161
160
|
for (const rect of getResolutionRects(resolution)) {
|
|
162
161
|
if (rect.width <= 0 || rect.height <= 0 || rect.bottom < 0 || rect.top > window.innerHeight) {
|
|
163
162
|
continue;
|
|
@@ -168,10 +167,7 @@ function createCommentsOverlay(options) {
|
|
|
168
167
|
} else {
|
|
169
168
|
highlight.dataset.commentDraft = "true";
|
|
170
169
|
}
|
|
171
|
-
highlight.style.cssText = `position:absolute;pointer-events:none;background:${HIGHLIGHT_BACKGROUND};border-radius:
|
|
172
|
-
if (isQuote) {
|
|
173
|
-
highlight.style.borderBottom = QUOTE_UNDERLINE;
|
|
174
|
-
}
|
|
170
|
+
highlight.style.cssText = `position:absolute;pointer-events:none;background:${threadId ? HIGHLIGHT_BACKGROUND : "transparent"};border:${isOutlined ? HIGHLIGHT_OUTLINE : "0"};border-radius:4px;box-sizing:border-box`;
|
|
175
171
|
highlight.style.left = `${rect.left}px`;
|
|
176
172
|
highlight.style.top = `${rect.top}px`;
|
|
177
173
|
highlight.style.width = `${rect.width}px`;
|
|
@@ -180,7 +176,7 @@ function createCommentsOverlay(options) {
|
|
|
180
176
|
}
|
|
181
177
|
};
|
|
182
178
|
const isHighlightShown = (threadId) => {
|
|
183
|
-
return mode === "comment" || threadId ===
|
|
179
|
+
return mode === "comment" || selection?.kind === "thread" && threadId === selection.threadId || threadId === hoveredBadgeThreadId;
|
|
184
180
|
};
|
|
185
181
|
const createIcon = (size, path) => {
|
|
186
182
|
const icon = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
@@ -320,7 +316,7 @@ function createCommentsOverlay(options) {
|
|
|
320
316
|
};
|
|
321
317
|
for (const pin of pins) {
|
|
322
318
|
const resolution = resolutions.get(pin.threadId);
|
|
323
|
-
const isFocused = pin.threadId ===
|
|
319
|
+
const isFocused = selection?.kind === "thread" && pin.threadId === selection.threadId;
|
|
324
320
|
if (!resolution || pin.status === "resolved" && !isFocused || !isResolutionVisible(resolution)) {
|
|
325
321
|
continue;
|
|
326
322
|
}
|
|
@@ -350,12 +346,16 @@ function createCommentsOverlay(options) {
|
|
|
350
346
|
badge.style.top = `${position.top}px`;
|
|
351
347
|
const isActive = isFocused || pin.threadId === hoveredBadgeThreadId;
|
|
352
348
|
const dot = document.createElement("span");
|
|
353
|
-
dot.style.cssText = `display:flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;min-width:48px;height:${BADGE_SIZE_PX}px;padding:0 6px;border-radius:
|
|
349
|
+
dot.style.cssText = `display:flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;min-width:48px;height:${BADGE_SIZE_PX}px;padding:0 6px;border-radius:999px;border:1px solid ${SOFT_OUTLINE_COLOR};background:var(--replo-comment-surface);color:var(--replo-comment-text);box-shadow:0 1px 3px rgba(0,0,0,.08);font:600 14px/1 system-ui,-apple-system,sans-serif;font-variant-numeric:tabular-nums;white-space:nowrap`;
|
|
354
350
|
if (isCompact) {
|
|
355
351
|
dot.style.marginRight = "3px";
|
|
356
352
|
}
|
|
357
353
|
if (isActive) {
|
|
358
|
-
dot.style.borderColor = "
|
|
354
|
+
dot.style.borderColor = "var(--replo-comment-accent)";
|
|
355
|
+
}
|
|
356
|
+
if (isFocused) {
|
|
357
|
+
dot.style.background = "var(--replo-comment-accent)";
|
|
358
|
+
dot.style.color = "var(--replo-comment-on-accent)";
|
|
359
359
|
}
|
|
360
360
|
if (resolution.status === "ambiguous") {
|
|
361
361
|
dot.style.borderStyle = "dashed";
|
|
@@ -380,8 +380,8 @@ function createCommentsOverlay(options) {
|
|
|
380
380
|
child.remove();
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
-
if (
|
|
384
|
-
paintHighlight(
|
|
383
|
+
if (selection?.kind === "draft" && selection.resolution && isResolutionVisible(selection.resolution)) {
|
|
384
|
+
paintHighlight(selection.resolution, null);
|
|
385
385
|
}
|
|
386
386
|
paintBadges();
|
|
387
387
|
paintHover();
|
|
@@ -409,21 +409,23 @@ function createCommentsOverlay(options) {
|
|
|
409
409
|
const anchor = captureAnchor(target, point);
|
|
410
410
|
const range = "tagName" in target ? void 0 : target;
|
|
411
411
|
const rect = (range ?? element).getBoundingClientRect();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
412
|
+
selection = {
|
|
413
|
+
kind: "draft",
|
|
414
|
+
anchor,
|
|
415
|
+
resolution: {
|
|
416
|
+
status: "attached",
|
|
417
|
+
element,
|
|
418
|
+
...range ? { range } : {}
|
|
419
|
+
}
|
|
418
420
|
};
|
|
419
421
|
hoveredElement = null;
|
|
420
422
|
void finishAnchor(anchor).then((prepared) => report(prepared, rect));
|
|
421
423
|
schedulePaint();
|
|
422
424
|
};
|
|
423
425
|
const clearDraft = () => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
426
|
+
if (selection?.kind === "draft") {
|
|
427
|
+
selection = null;
|
|
428
|
+
}
|
|
427
429
|
};
|
|
428
430
|
const refreshSelectionPrompt = () => {
|
|
429
431
|
selectionTimer = null;
|
|
@@ -595,12 +597,12 @@ function createCommentsOverlay(options) {
|
|
|
595
597
|
if (event.key !== "Escape") {
|
|
596
598
|
return;
|
|
597
599
|
}
|
|
598
|
-
if (mode === "comment" ||
|
|
600
|
+
if (mode === "comment" || selection?.kind === "draft") {
|
|
599
601
|
if (mode === "comment") {
|
|
600
602
|
event.preventDefault();
|
|
601
603
|
event.stopImmediatePropagation();
|
|
602
604
|
}
|
|
603
|
-
if (
|
|
605
|
+
if (selection?.kind === "draft") {
|
|
604
606
|
clearDraft();
|
|
605
607
|
options.onPickCancelled();
|
|
606
608
|
schedulePaint();
|
|
@@ -713,7 +715,9 @@ function createCommentsOverlay(options) {
|
|
|
713
715
|
mode = nextMode;
|
|
714
716
|
selectionPrompt = null;
|
|
715
717
|
hoveredElement = null;
|
|
716
|
-
|
|
718
|
+
if (selection?.kind === "thread") {
|
|
719
|
+
selection = null;
|
|
720
|
+
}
|
|
717
721
|
setCursor(nextMode, previousMode);
|
|
718
722
|
if (previousMode === "comment") {
|
|
719
723
|
window.getSelection()?.removeAllRanges();
|
|
@@ -731,7 +735,7 @@ function createCommentsOverlay(options) {
|
|
|
731
735
|
resolveAll();
|
|
732
736
|
},
|
|
733
737
|
focusThread: (threadId) => {
|
|
734
|
-
|
|
738
|
+
selection = { kind: "thread", threadId };
|
|
735
739
|
const resolution = resolutions.get(threadId);
|
|
736
740
|
const target = resolution?.range?.startContainer.parentElement ?? resolution?.element ?? null;
|
|
737
741
|
if (resolution && isResolutionVisible(resolution)) {
|
|
@@ -748,7 +752,11 @@ function createCommentsOverlay(options) {
|
|
|
748
752
|
schedulePaint();
|
|
749
753
|
},
|
|
750
754
|
setDraftAnchor: (anchor) => {
|
|
751
|
-
|
|
755
|
+
if (anchor?.type === "element") {
|
|
756
|
+
selection = { kind: "draft", anchor, resolution: null };
|
|
757
|
+
} else {
|
|
758
|
+
clearDraft();
|
|
759
|
+
}
|
|
752
760
|
resolveAll();
|
|
753
761
|
},
|
|
754
762
|
refresh: resolveAll,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.2",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sandbox-runtime",
|
|
6
|
-
"version": "1.24.
|
|
6
|
+
"version": "1.24.2",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-11-
|
|
8
|
+
"commit": "a3a1a0505f42cea89e63bbfa5628b1d35558fc76",
|
|
9
|
+
"builtAt": "2026-11-03T19:15:42.310Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|