@lowdefy/server-dev 5.6.0 → 6.0.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/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +122 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +61 -47
- package/package.original.json +64 -50
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +235 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { toPng } from 'html-to-image';
|
|
18
|
+
|
|
19
|
+
import drawAnnotationsSvg from './drawAnnotationsSvg.js';
|
|
20
|
+
import pinResponsiveImages from './pinResponsiveImages.js';
|
|
21
|
+
|
|
22
|
+
// Chrome caps canvas dimensions well above this, but rasterizing a very long
|
|
23
|
+
// page at retina density burns memory for no feedback value — drop to 1x
|
|
24
|
+
// instead of failing.
|
|
25
|
+
const MAX_CANVAS_DIMENSION = 16000;
|
|
26
|
+
|
|
27
|
+
function resolveBackgroundColor() {
|
|
28
|
+
try {
|
|
29
|
+
const bodyBg = getComputedStyle(document.body).backgroundColor;
|
|
30
|
+
if (bodyBg && bodyBg !== 'rgba(0, 0, 0, 0)' && bodyBg !== 'transparent') {
|
|
31
|
+
return bodyBg;
|
|
32
|
+
}
|
|
33
|
+
const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
|
|
34
|
+
if (htmlBg && htmlBg !== 'rgba(0, 0, 0, 0)' && htmlBg !== 'transparent') {
|
|
35
|
+
return htmlBg;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through to the default.
|
|
39
|
+
}
|
|
40
|
+
return '#ffffff';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Rasterizes the developer's actual tab — theme, loaded data, exact pixels —
|
|
44
|
+
// with the batch's annotations drawn on top, and returns a PNG data URL the
|
|
45
|
+
// batch POSTs to the dev server to save. This replaces re-rendering the page
|
|
46
|
+
// headless, which diverged from what the developer was looking at (light
|
|
47
|
+
// theme, un-settled requests, no client-only state). Returns null on any
|
|
48
|
+
// failure so the server can fall back to the headless capture.
|
|
49
|
+
async function captureTabScreenshot({ annotations }) {
|
|
50
|
+
let restoreImages = () => {};
|
|
51
|
+
try {
|
|
52
|
+
restoreImages = pinResponsiveImages();
|
|
53
|
+
drawAnnotationsSvg(annotations ?? []);
|
|
54
|
+
const pixelRatio =
|
|
55
|
+
Math.max(document.documentElement.scrollWidth, document.documentElement.scrollHeight) *
|
|
56
|
+
(window.devicePixelRatio || 1) >
|
|
57
|
+
MAX_CANVAS_DIMENSION
|
|
58
|
+
? 1
|
|
59
|
+
: window.devicePixelRatio || 1;
|
|
60
|
+
const dataUrl = await toPng(document.documentElement, {
|
|
61
|
+
// Keep everything except the overlay's own UI (panels, tray, chips).
|
|
62
|
+
// The annotation SVG lives outside the overlay root, so it stays in.
|
|
63
|
+
filter: (node) =>
|
|
64
|
+
!(node instanceof Element && node.hasAttribute('data-lowdefy-feedback')),
|
|
65
|
+
pixelRatio,
|
|
66
|
+
backgroundColor: resolveBackgroundColor(),
|
|
67
|
+
});
|
|
68
|
+
if (typeof dataUrl === 'string' && dataUrl.startsWith('data:image/png')) {
|
|
69
|
+
return dataUrl;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
} finally {
|
|
75
|
+
try {
|
|
76
|
+
restoreImages();
|
|
77
|
+
document.getElementById('lowdefy-feedback-annotations-svg')?.remove();
|
|
78
|
+
} catch {
|
|
79
|
+
// Never let cleanup break the send flow.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default captureTabScreenshot;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// navigator.clipboard needs a secure context — localhost qualifies, so the
|
|
18
|
+
// async API is the normal path in dev. The execCommand fallback covers
|
|
19
|
+
// non-localhost LAN dev hosts (http://192.168...) where it does not.
|
|
20
|
+
async function copyToClipboard({ text }) {
|
|
21
|
+
try {
|
|
22
|
+
if (window.navigator?.clipboard?.writeText) {
|
|
23
|
+
await window.navigator.clipboard.writeText(text);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Fall through to the legacy path.
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const textarea = document.createElement('textarea');
|
|
31
|
+
textarea.value = text;
|
|
32
|
+
textarea.setAttribute('data-lowdefy-feedback', '');
|
|
33
|
+
textarea.style.position = 'fixed';
|
|
34
|
+
textarea.style.opacity = '0';
|
|
35
|
+
document.body.appendChild(textarea);
|
|
36
|
+
textarea.select();
|
|
37
|
+
const ok = document.execCommand('copy');
|
|
38
|
+
document.body.removeChild(textarea);
|
|
39
|
+
return ok;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default copyToClipboard;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Draws a batch's annotations (element outlines with index labels, plus the
|
|
18
|
+
// drawn rect/arrow/freehand shapes) as an SVG layer appended to document.body,
|
|
19
|
+
// id "lowdefy-feedback-annotations-svg" — callers remove it by id when done.
|
|
20
|
+
//
|
|
21
|
+
// Shared by two capture paths, so it MUST stay fully self-contained (no outer
|
|
22
|
+
// imports or closures): captureTabScreenshot.js calls it directly in the
|
|
23
|
+
// developer's tab, and captureAnnotatedScreenshot.js passes it to Playwright's
|
|
24
|
+
// page.evaluate, which serializes the function source into the headless page.
|
|
25
|
+
//
|
|
26
|
+
// Geometry is viewport-relative at the recorded scroll offset. The layer is
|
|
27
|
+
// positioned absolute at the CURRENT scroll offset (document coordinates
|
|
28
|
+
// covering the visible viewport box), which lands the shapes correctly both in
|
|
29
|
+
// a full-document rasterization (in-tab) and in a viewport screenshot after
|
|
30
|
+
// scrolling to the recorded offset (headless).
|
|
31
|
+
function drawAnnotationsSvg(annotations) {
|
|
32
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
33
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
34
|
+
svg.setAttribute('id', 'lowdefy-feedback-annotations-svg');
|
|
35
|
+
svg.setAttribute(
|
|
36
|
+
'style',
|
|
37
|
+
`position:absolute;left:${window.scrollX}px;top:${window.scrollY}px;` +
|
|
38
|
+
`width:${window.innerWidth}px;height:${window.innerHeight}px;` +
|
|
39
|
+
'pointer-events:none;z-index:2147483000;'
|
|
40
|
+
);
|
|
41
|
+
const defs = document.createElementNS(NS, 'defs');
|
|
42
|
+
const marker = document.createElementNS(NS, 'marker');
|
|
43
|
+
marker.setAttribute('id', 'ldf-annotation-arrowhead');
|
|
44
|
+
marker.setAttribute('markerWidth', '10');
|
|
45
|
+
marker.setAttribute('markerHeight', '8');
|
|
46
|
+
marker.setAttribute('refX', '9');
|
|
47
|
+
marker.setAttribute('refY', '4');
|
|
48
|
+
marker.setAttribute('orient', 'auto');
|
|
49
|
+
const head = document.createElementNS(NS, 'polygon');
|
|
50
|
+
head.setAttribute('points', '0 0, 10 4, 0 8');
|
|
51
|
+
head.setAttribute('fill', '#ff4785');
|
|
52
|
+
marker.appendChild(head);
|
|
53
|
+
defs.appendChild(marker);
|
|
54
|
+
svg.appendChild(defs);
|
|
55
|
+
|
|
56
|
+
const stroke = '#ff4785';
|
|
57
|
+
annotations.forEach((annotation, index) => {
|
|
58
|
+
const rect = annotation.geometry?.elementRect;
|
|
59
|
+
if (rect) {
|
|
60
|
+
const outline = document.createElementNS(NS, 'rect');
|
|
61
|
+
outline.setAttribute('x', rect.x);
|
|
62
|
+
outline.setAttribute('y', rect.y);
|
|
63
|
+
outline.setAttribute('width', rect.width);
|
|
64
|
+
outline.setAttribute('height', rect.height);
|
|
65
|
+
outline.setAttribute('fill', 'rgba(255,71,133,0.08)');
|
|
66
|
+
outline.setAttribute('stroke', stroke);
|
|
67
|
+
outline.setAttribute('stroke-width', '2');
|
|
68
|
+
svg.appendChild(outline);
|
|
69
|
+
const label = document.createElementNS(NS, 'text');
|
|
70
|
+
label.setAttribute('x', rect.x);
|
|
71
|
+
label.setAttribute('y', Math.max(12, rect.y - 6));
|
|
72
|
+
label.setAttribute('fill', stroke);
|
|
73
|
+
label.setAttribute('font-size', '12');
|
|
74
|
+
label.setAttribute('font-family', 'system-ui, sans-serif');
|
|
75
|
+
label.setAttribute('font-weight', '600');
|
|
76
|
+
label.textContent = `${index + 1}`;
|
|
77
|
+
svg.appendChild(label);
|
|
78
|
+
}
|
|
79
|
+
(annotation.geometry?.shapes ?? []).forEach((shape) => {
|
|
80
|
+
if (shape.type === 'rect' && shape.points?.length >= 2) {
|
|
81
|
+
const [a, b] = shape.points;
|
|
82
|
+
const el = document.createElementNS(NS, 'rect');
|
|
83
|
+
el.setAttribute('x', Math.min(a.x, b.x));
|
|
84
|
+
el.setAttribute('y', Math.min(a.y, b.y));
|
|
85
|
+
el.setAttribute('width', Math.abs(b.x - a.x));
|
|
86
|
+
el.setAttribute('height', Math.abs(b.y - a.y));
|
|
87
|
+
el.setAttribute('fill', 'rgba(255,71,133,0.08)');
|
|
88
|
+
el.setAttribute('stroke', stroke);
|
|
89
|
+
el.setAttribute('stroke-width', '2.5');
|
|
90
|
+
svg.appendChild(el);
|
|
91
|
+
}
|
|
92
|
+
if (shape.type === 'arrow' && shape.points?.length >= 2) {
|
|
93
|
+
const [tail, headPoint] = shape.points;
|
|
94
|
+
const el = document.createElementNS(NS, 'line');
|
|
95
|
+
el.setAttribute('x1', tail.x);
|
|
96
|
+
el.setAttribute('y1', tail.y);
|
|
97
|
+
el.setAttribute('x2', headPoint.x);
|
|
98
|
+
el.setAttribute('y2', headPoint.y);
|
|
99
|
+
el.setAttribute('stroke', stroke);
|
|
100
|
+
el.setAttribute('stroke-width', '2.5');
|
|
101
|
+
el.setAttribute('marker-end', 'url(#ldf-annotation-arrowhead)');
|
|
102
|
+
svg.appendChild(el);
|
|
103
|
+
}
|
|
104
|
+
if (shape.type === 'freehand' && shape.points?.length >= 2) {
|
|
105
|
+
const el = document.createElementNS(NS, 'polyline');
|
|
106
|
+
el.setAttribute('points', shape.points.map((p) => `${p.x},${p.y}`).join(' '));
|
|
107
|
+
el.setAttribute('fill', 'none');
|
|
108
|
+
el.setAttribute('stroke', stroke);
|
|
109
|
+
el.setAttribute('stroke-width', '2.5');
|
|
110
|
+
el.setAttribute('stroke-linejoin', 'round');
|
|
111
|
+
el.setAttribute('stroke-linecap', 'round');
|
|
112
|
+
svg.appendChild(el);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
document.body.appendChild(svg);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default drawAnnotationsSvg;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Pure DOM-reading helpers for the feedback overlay's "pick a block" mode.
|
|
18
|
+
// Blocks render as id="bl-{blockId}" (skeletons use "s-bl-" and must NOT
|
|
19
|
+
// match — the "^=" prefix selector already excludes them since "s-bl-..."
|
|
20
|
+
// starts with "s-", not "bl-").
|
|
21
|
+
|
|
22
|
+
const BLOCK_ID_PREFIX = 'bl-';
|
|
23
|
+
const BLOCK_SELECTOR = `[id^="${BLOCK_ID_PREFIX}"]`;
|
|
24
|
+
const FEEDBACK_ROOT_SELECTOR = '[data-lowdefy-feedback]';
|
|
25
|
+
const MAX_TEXT_LENGTH = 120;
|
|
26
|
+
const MAX_CLASS_LENGTH = 200;
|
|
27
|
+
|
|
28
|
+
function isInsideFeedbackOverlay(el) {
|
|
29
|
+
if (!(el instanceof Element)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return Boolean(el.closest(FEEDBACK_ROOT_SELECTOR));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripBlockId(blockEl) {
|
|
36
|
+
return blockEl.id.slice(BLOCK_ID_PREFIX.length);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Nearest ancestor block element (or the element itself), ignoring the
|
|
40
|
+
// feedback overlay's own DOM so pointing at the overlay never resolves to an
|
|
41
|
+
// app block.
|
|
42
|
+
function nearestBlock(el) {
|
|
43
|
+
if (!(el instanceof Element) || isInsideFeedbackOverlay(el)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const blockEl = el.closest(BLOCK_SELECTOR);
|
|
47
|
+
if (!blockEl) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return stripBlockId(blockEl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// All bl- ancestors, nearest to outermost, prefix stripped.
|
|
54
|
+
function blockAncestorChain(el) {
|
|
55
|
+
if (!(el instanceof Element) || isInsideFeedbackOverlay(el)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const chain = [];
|
|
59
|
+
let current = el.closest(BLOCK_SELECTOR);
|
|
60
|
+
while (current) {
|
|
61
|
+
chain.push(stripBlockId(current));
|
|
62
|
+
current = current.parentElement ? current.parentElement.closest(BLOCK_SELECTOR) : null;
|
|
63
|
+
}
|
|
64
|
+
return chain;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function describeElement(el) {
|
|
68
|
+
if (!(el instanceof Element)) {
|
|
69
|
+
return { tag: null, classes: null, text: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let classes = null;
|
|
73
|
+
try {
|
|
74
|
+
classes = typeof el.className === 'string' ? el.className : null;
|
|
75
|
+
} catch {
|
|
76
|
+
classes = null;
|
|
77
|
+
}
|
|
78
|
+
if (classes && classes.length > MAX_CLASS_LENGTH) {
|
|
79
|
+
classes = `${classes.slice(0, MAX_CLASS_LENGTH)}…`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let text = '';
|
|
83
|
+
try {
|
|
84
|
+
text = el.textContent ? el.textContent.trim() : '';
|
|
85
|
+
} catch {
|
|
86
|
+
text = '';
|
|
87
|
+
}
|
|
88
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
89
|
+
text = `${text.slice(0, MAX_TEXT_LENGTH)}…`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
tag: el.tagName ?? null,
|
|
94
|
+
classes,
|
|
95
|
+
text: text || null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { nearestBlock, blockAncestorChain, describeElement };
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Visual language: subtle dark chrome panels floating over the app, kept
|
|
18
|
+
// above ErrorBar.jsx (bottom: 40px, zIndex 99999) but under nothing — this
|
|
19
|
+
// overlay is the topmost thing on the page while active.
|
|
20
|
+
|
|
21
|
+
const STYLE_TAG_ID = 'lowdefy-feedback-style-tag';
|
|
22
|
+
|
|
23
|
+
const colors = {
|
|
24
|
+
panelBg: 'rgba(20,20,25,0.92)',
|
|
25
|
+
panelBorder: 'rgba(255,255,255,0.12)',
|
|
26
|
+
text: '#ffffff',
|
|
27
|
+
textMuted: 'rgba(255,255,255,0.6)',
|
|
28
|
+
accent: '#4f9cf9',
|
|
29
|
+
accentBg: 'rgba(79,156,249,0.08)',
|
|
30
|
+
stroke: '#ff4785',
|
|
31
|
+
strokeBg: 'rgba(255,71,133,0.08)',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const fontFamily =
|
|
35
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
36
|
+
|
|
37
|
+
const overlayContainer = {
|
|
38
|
+
position: 'fixed',
|
|
39
|
+
inset: 0,
|
|
40
|
+
zIndex: 2147483000,
|
|
41
|
+
fontFamily,
|
|
42
|
+
fontSize: 13,
|
|
43
|
+
color: colors.text,
|
|
44
|
+
pointerEvents: 'none',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const highlightBox = {
|
|
48
|
+
position: 'fixed',
|
|
49
|
+
border: `2px solid ${colors.accent}`,
|
|
50
|
+
backgroundColor: colors.accentBg,
|
|
51
|
+
borderRadius: 4,
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
transition: 'top 60ms ease, left 60ms ease, width 60ms ease, height 60ms ease',
|
|
54
|
+
boxSizing: 'border-box',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const selectedHighlightBox = {
|
|
58
|
+
...highlightBox,
|
|
59
|
+
border: `2px solid ${colors.stroke}`,
|
|
60
|
+
backgroundColor: colors.strokeBg,
|
|
61
|
+
transition: 'none',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const labelChip = {
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
top: -22,
|
|
67
|
+
left: -2,
|
|
68
|
+
backgroundColor: colors.accent,
|
|
69
|
+
color: '#fff',
|
|
70
|
+
fontSize: 11,
|
|
71
|
+
lineHeight: '16px',
|
|
72
|
+
padding: '2px 6px',
|
|
73
|
+
borderRadius: 4,
|
|
74
|
+
whiteSpace: 'nowrap',
|
|
75
|
+
pointerEvents: 'none',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const selectedLabelChip = {
|
|
79
|
+
...labelChip,
|
|
80
|
+
backgroundColor: colors.stroke,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const panelBase = {
|
|
84
|
+
position: 'fixed',
|
|
85
|
+
width: 320,
|
|
86
|
+
backgroundColor: colors.panelBg,
|
|
87
|
+
border: `1px solid ${colors.panelBorder}`,
|
|
88
|
+
borderRadius: 8,
|
|
89
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
|
|
90
|
+
padding: 14,
|
|
91
|
+
boxSizing: 'border-box',
|
|
92
|
+
pointerEvents: 'auto',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const annotatingPanel = {
|
|
96
|
+
...panelBase,
|
|
97
|
+
bottom: 40,
|
|
98
|
+
right: 16,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const reviewTray = {
|
|
102
|
+
...panelBase,
|
|
103
|
+
bottom: 40,
|
|
104
|
+
left: 16,
|
|
105
|
+
width: 300,
|
|
106
|
+
maxHeight: '60vh',
|
|
107
|
+
overflowY: 'auto',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const sentBanner = {
|
|
111
|
+
...panelBase,
|
|
112
|
+
bottom: 40,
|
|
113
|
+
right: 16,
|
|
114
|
+
textAlign: 'center',
|
|
115
|
+
color: '#7ee787',
|
|
116
|
+
fontWeight: 600,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const panelHeading = {
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
fontWeight: 600,
|
|
122
|
+
marginBottom: 8,
|
|
123
|
+
color: colors.textMuted,
|
|
124
|
+
textTransform: 'uppercase',
|
|
125
|
+
letterSpacing: '0.04em',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const descriptorRow = {
|
|
129
|
+
fontSize: 12,
|
|
130
|
+
color: colors.textMuted,
|
|
131
|
+
marginBottom: 10,
|
|
132
|
+
wordBreak: 'break-word',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const textareaStyle = {
|
|
136
|
+
width: '100%',
|
|
137
|
+
minHeight: 72,
|
|
138
|
+
resize: 'vertical',
|
|
139
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
140
|
+
border: `1px solid ${colors.panelBorder}`,
|
|
141
|
+
borderRadius: 6,
|
|
142
|
+
color: colors.text,
|
|
143
|
+
fontFamily,
|
|
144
|
+
fontSize: 13,
|
|
145
|
+
padding: 8,
|
|
146
|
+
boxSizing: 'border-box',
|
|
147
|
+
marginBottom: 10,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const toolRow = {
|
|
151
|
+
display: 'flex',
|
|
152
|
+
gap: 6,
|
|
153
|
+
marginBottom: 10,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function toolButtonStyle(active) {
|
|
157
|
+
return {
|
|
158
|
+
flex: 1,
|
|
159
|
+
padding: '6px 0',
|
|
160
|
+
borderRadius: 6,
|
|
161
|
+
border: `1px solid ${active ? colors.accent : colors.panelBorder}`,
|
|
162
|
+
backgroundColor: active ? colors.accentBg : 'rgba(255,255,255,0.04)',
|
|
163
|
+
color: colors.text,
|
|
164
|
+
cursor: 'pointer',
|
|
165
|
+
fontSize: 12,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const buttonRow = {
|
|
170
|
+
display: 'flex',
|
|
171
|
+
gap: 8,
|
|
172
|
+
justifyContent: 'flex-end',
|
|
173
|
+
flexWrap: 'wrap',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const primaryButton = {
|
|
177
|
+
padding: '6px 14px',
|
|
178
|
+
borderRadius: 6,
|
|
179
|
+
border: 'none',
|
|
180
|
+
backgroundColor: colors.accent,
|
|
181
|
+
color: '#fff',
|
|
182
|
+
cursor: 'pointer',
|
|
183
|
+
fontSize: 12,
|
|
184
|
+
fontWeight: 600,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const secondaryButton = {
|
|
188
|
+
padding: '6px 14px',
|
|
189
|
+
borderRadius: 6,
|
|
190
|
+
border: `1px solid ${colors.panelBorder}`,
|
|
191
|
+
backgroundColor: 'transparent',
|
|
192
|
+
color: colors.textMuted,
|
|
193
|
+
cursor: 'pointer',
|
|
194
|
+
fontSize: 12,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const dangerButton = {
|
|
198
|
+
...secondaryButton,
|
|
199
|
+
color: '#ff8080',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const reviewItem = {
|
|
203
|
+
display: 'flex',
|
|
204
|
+
justifyContent: 'space-between',
|
|
205
|
+
alignItems: 'flex-start',
|
|
206
|
+
gap: 8,
|
|
207
|
+
padding: '8px 0',
|
|
208
|
+
borderBottom: `1px solid ${colors.panelBorder}`,
|
|
209
|
+
fontSize: 12,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const reviewItemRemove = {
|
|
213
|
+
background: 'none',
|
|
214
|
+
border: 'none',
|
|
215
|
+
color: colors.textMuted,
|
|
216
|
+
cursor: 'pointer',
|
|
217
|
+
fontSize: 14,
|
|
218
|
+
lineHeight: 1,
|
|
219
|
+
padding: 0,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const consoleCountRow = {
|
|
223
|
+
fontSize: 11,
|
|
224
|
+
color: colors.textMuted,
|
|
225
|
+
margin: '10px 0',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const errorRow = {
|
|
229
|
+
fontSize: 11,
|
|
230
|
+
color: '#ff8080',
|
|
231
|
+
marginBottom: 8,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const svgLayer = {
|
|
235
|
+
position: 'fixed',
|
|
236
|
+
inset: 0,
|
|
237
|
+
width: '100%',
|
|
238
|
+
height: '100%',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// A few rules inline styles can't express: forcing cursor over arbitrary app
|
|
242
|
+
// descendants during pick/draw modes and a placeholder color. The arrowhead
|
|
243
|
+
// SVG marker is defined inline in the overlay's <defs> instead — no
|
|
244
|
+
// stylesheet needed for that.
|
|
245
|
+
function injectFeedbackStyleTag() {
|
|
246
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_TAG_ID)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const style = document.createElement('style');
|
|
250
|
+
style.id = STYLE_TAG_ID;
|
|
251
|
+
style.textContent = `
|
|
252
|
+
.lowdefy-feedback-picking, .lowdefy-feedback-picking * {
|
|
253
|
+
cursor: crosshair !important;
|
|
254
|
+
}
|
|
255
|
+
.lowdefy-feedback-drawing, .lowdefy-feedback-drawing * {
|
|
256
|
+
cursor: crosshair !important;
|
|
257
|
+
}
|
|
258
|
+
[data-lowdefy-feedback] textarea::placeholder {
|
|
259
|
+
color: rgba(255,255,255,0.4);
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
document.head.appendChild(style);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function removeFeedbackStyleTag() {
|
|
266
|
+
if (typeof document === 'undefined') {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const existing = document.getElementById(STYLE_TAG_ID);
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.remove();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
colors,
|
|
277
|
+
overlayContainer,
|
|
278
|
+
highlightBox,
|
|
279
|
+
selectedHighlightBox,
|
|
280
|
+
labelChip,
|
|
281
|
+
selectedLabelChip,
|
|
282
|
+
annotatingPanel,
|
|
283
|
+
reviewTray,
|
|
284
|
+
sentBanner,
|
|
285
|
+
panelHeading,
|
|
286
|
+
descriptorRow,
|
|
287
|
+
textareaStyle,
|
|
288
|
+
toolRow,
|
|
289
|
+
toolButtonStyle,
|
|
290
|
+
buttonRow,
|
|
291
|
+
primaryButton,
|
|
292
|
+
secondaryButton,
|
|
293
|
+
dangerButton,
|
|
294
|
+
reviewItem,
|
|
295
|
+
reviewItemRemove,
|
|
296
|
+
consoleCountRow,
|
|
297
|
+
errorRow,
|
|
298
|
+
svgLayer,
|
|
299
|
+
injectFeedbackStyleTag,
|
|
300
|
+
removeFeedbackStyleTag,
|
|
301
|
+
};
|