@lowdefy/server-dev 5.6.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/DevStreamContext.js +30 -0
  3. package/client/Inspector.jsx +226 -0
  4. package/{lib/client/Page.js → client/Page.jsx} +4 -8
  5. package/client/Reload.jsx +85 -0
  6. package/client/Routing.jsx +117 -0
  7. package/client/clientBuildImports.test.mjs +61 -0
  8. package/client/feedback/FeedbackMount.jsx +83 -0
  9. package/client/feedback/FeedbackOverlay.jsx +901 -0
  10. package/client/feedback/captureTabScreenshot.js +84 -0
  11. package/client/feedback/copyToClipboard.js +45 -0
  12. package/client/feedback/drawAnnotationsSvg.js +119 -0
  13. package/client/feedback/elementInspect.js +99 -0
  14. package/client/feedback/feedbackStyles.js +301 -0
  15. package/client/feedback/pinResponsiveImages.js +59 -0
  16. package/client/feedback/sendFeedback.js +39 -0
  17. package/client/feedback/useFeedbackToggle.js +70 -0
  18. package/client/main.jsx +42 -0
  19. package/client/openInEditor/OpenInEditorListener.jsx +268 -0
  20. package/client/openInEditor/findBlockLocation.js +51 -0
  21. package/lib/build/app.js +8 -1
  22. package/lib/build/appMeta.js +8 -1
  23. package/lib/build/auth.js +8 -1
  24. package/lib/build/config.js +8 -1
  25. package/lib/build/i18n.js +8 -1
  26. package/lib/build/logger.js +8 -1
  27. package/lib/build/theme.js +8 -1
  28. package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
  29. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
  30. package/lib/client/setPageId.js +6 -5
  31. package/lib/client/utils/useMutateCache.js +14 -1
  32. package/lib/client/utils/usePageConfig.js +45 -43
  33. package/lib/client/utils/usePageConfig.test.mjs +62 -0
  34. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  35. package/lib/docs/checkpointPaths.js +91 -0
  36. package/lib/docs/checkpointStore.js +240 -0
  37. package/lib/docs/checkpointToMocks.js +65 -0
  38. package/lib/docs/clientErrorStore.js +36 -0
  39. package/lib/docs/configCheckpoints.test.mjs +156 -0
  40. package/lib/docs/createConfigCheckpoint.js +92 -0
  41. package/lib/docs/createDocsMcpServer.js +531 -0
  42. package/lib/docs/createDocsMcpServer.test.mjs +141 -0
  43. package/lib/docs/devMockRegistry.js +68 -0
  44. package/lib/docs/docs.test.mjs +448 -0
  45. package/lib/docs/enrichFeedback.js +71 -0
  46. package/lib/docs/evalOperator.js +47 -0
  47. package/lib/docs/evalOperator.test.mjs +157 -0
  48. package/lib/docs/evalOperatorHeadless.js +87 -0
  49. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  50. package/lib/docs/evalOperatorInTab.js +48 -0
  51. package/lib/docs/findConfig.js +260 -0
  52. package/lib/docs/formatFeedback.js +121 -0
  53. package/lib/docs/formatFeedback.test.mjs +204 -0
  54. package/lib/docs/getAppMap.js +180 -0
  55. package/lib/docs/getBrowser.js +127 -0
  56. package/lib/docs/getBrowser.test.mjs +122 -0
  57. package/lib/docs/getBuildStatus.js +37 -0
  58. package/lib/docs/getCoreDoc.js +63 -0
  59. package/lib/docs/getDocsManifest.js +40 -0
  60. package/lib/docs/getExamples.js +58 -0
  61. package/lib/docs/getOverview.js +115 -0
  62. package/lib/docs/getPageConfig.js +53 -0
  63. package/lib/docs/getPluginDoc.js +55 -0
  64. package/lib/docs/getSchema.js +65 -0
  65. package/lib/docs/inspectState.js +47 -0
  66. package/lib/docs/inspectState.test.mjs +140 -0
  67. package/lib/docs/inspectStateFromTab.js +40 -0
  68. package/lib/docs/inspectStateHeadless.js +84 -0
  69. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  70. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  71. package/lib/docs/listConfigCheckpoints.js +55 -0
  72. package/lib/docs/listPlugins.js +80 -0
  73. package/lib/docs/listTypes.js +105 -0
  74. package/lib/docs/loadState.js +152 -0
  75. package/lib/docs/mapPageBuildErrors.js +28 -0
  76. package/lib/docs/normalizeTypeKind.js +40 -0
  77. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  78. package/lib/docs/readPageArtifact.js +26 -0
  79. package/lib/docs/resolvePluginDir.js +49 -0
  80. package/lib/docs/resolveSource.js +48 -0
  81. package/lib/docs/resolveSource.test.mjs +100 -0
  82. package/lib/docs/revertConfigCheckpoint.js +73 -0
  83. package/lib/docs/runRequest.js +141 -0
  84. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  85. package/lib/docs/scaffoldPage.js +75 -0
  86. package/lib/docs/scaffoldPage.test.mjs +97 -0
  87. package/lib/docs/screenshotPage.js +110 -0
  88. package/lib/docs/screenshotPage.test.mjs +59 -0
  89. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  90. package/lib/docs/searchDocs.js +67 -0
  91. package/lib/docs/setupTestFixtures.mjs +335 -0
  92. package/lib/docs/snapshotState.js +59 -0
  93. package/lib/docs/stateCheckpoints.test.mjs +366 -0
  94. package/lib/docs/tabAvailable.js +28 -0
  95. package/lib/docs/tabChannel.js +119 -0
  96. package/lib/docs/tabChannel.test.mjs +130 -0
  97. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +9 -4
  98. package/lib/server/auth/getDevSession.js +70 -0
  99. package/lib/server/auth/getDevSession.test.mjs +138 -0
  100. package/lib/server/auth/getHeadlessUser.js +40 -0
  101. package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
  102. package/lib/server/auth/headlessUser.js +29 -0
  103. package/lib/server/auth/resolveHeadlessUser.js +38 -0
  104. package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
  105. package/lib/server/auth/session.js +39 -0
  106. package/lib/server/auth/strategies.js +47 -0
  107. package/lib/server/createLowdefyContext.js +128 -0
  108. package/lib/server/createLowdefyContext.test.mjs +117 -0
  109. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  110. package/lib/server/jitPageBuilder.js +125 -12
  111. package/lib/server/log/createHandleError.test.mjs +126 -0
  112. package/lib/server/log/createLogger.test.mjs +62 -0
  113. package/lib/server/scrubSecrets.js +23 -0
  114. package/manager/getContext.mjs +3 -7
  115. package/manager/processes/initialBuild.mjs +0 -2
  116. package/manager/processes/installPlugins.mjs +19 -11
  117. package/manager/processes/lowdefyBuild.mjs +29 -7
  118. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  119. package/manager/processes/readDotEnv.mjs +13 -1
  120. package/manager/processes/shutdownServer.mjs +6 -6
  121. package/manager/processes/startProxy.mjs +160 -0
  122. package/manager/processes/startProxy.test.mjs +102 -0
  123. package/manager/processes/startServer.mjs +33 -16
  124. package/manager/processes/startWatchers.mjs +2 -2
  125. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  126. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  127. package/manager/run.mjs +50 -0
  128. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  129. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  130. package/manager/utils/formatNoticeBox.mjs +30 -0
  131. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  132. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  133. package/manager/utils/writeBuildStatus.mjs +39 -0
  134. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  135. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  136. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  137. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  138. package/package.json +62 -49
  139. package/package.original.json +65 -52
  140. package/postcss.config.cjs +1 -0
  141. package/src/app.js +175 -0
  142. package/src/html/renderDevPage.js +86 -0
  143. package/src/lib/getPathSegments.js +25 -0
  144. package/src/lib/safeScriptJson.js +38 -0
  145. package/src/middleware/apiContext.js +32 -0
  146. package/src/middleware/errorHandler.js +48 -0
  147. package/src/middleware/errorHandler.test.mjs +247 -0
  148. package/src/routes/agent.js +64 -0
  149. package/src/routes/apiPage.js +37 -0
  150. package/src/routes/auth.js +49 -0
  151. package/src/routes/auth.test.mjs +84 -0
  152. package/src/routes/clientError.js +57 -0
  153. package/src/routes/cron.js +47 -0
  154. package/src/routes/cronForward.js +47 -0
  155. package/src/routes/detached.js +47 -0
  156. package/src/routes/devInspect.js +111 -0
  157. package/src/routes/devInspect.test.mjs +119 -0
  158. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  159. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  160. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  161. package/src/routes/docs/checkpointsCreate.js +28 -0
  162. package/src/routes/docs/checkpointsList.js +23 -0
  163. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  164. package/src/routes/docs/content.js +31 -0
  165. package/src/routes/docs/evalOperator.js +56 -0
  166. package/src/routes/docs/examples.js +33 -0
  167. package/src/routes/docs/find.js +30 -0
  168. package/src/routes/docs/index.js +23 -0
  169. package/src/routes/docs/inspectState.js +44 -0
  170. package/src/routes/docs/loadState.js +49 -0
  171. package/src/routes/docs/mcp.js +30 -0
  172. package/src/routes/docs/pageConfig.js +36 -0
  173. package/src/routes/docs/parseUserParam.js +52 -0
  174. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  175. package/src/routes/docs/pluginDoc.js +33 -0
  176. package/src/routes/docs/plugins.js +23 -0
  177. package/src/routes/docs/runRequest.js +36 -0
  178. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  179. package/src/routes/docs/screenshot.js +72 -0
  180. package/src/routes/docs/search.js +27 -0
  181. package/src/routes/docs/snapshotState.js +36 -0
  182. package/src/routes/docs/stateCheckpointsList.js +26 -0
  183. package/src/routes/docs/types.js +33 -0
  184. package/src/routes/endpoints.js +71 -0
  185. package/src/routes/feedback.js +110 -0
  186. package/src/routes/feedback.test.mjs +204 -0
  187. package/src/routes/jitPage.js +86 -0
  188. package/src/routes/mcp.js +34 -0
  189. package/{pages/api → src/routes}/ping.js +4 -2
  190. package/src/routes/reload.js +79 -0
  191. package/src/routes/request.js +74 -0
  192. package/src/routes/request.test.mjs +124 -0
  193. package/{pages/api → src/routes}/root.js +4 -4
  194. package/src/routes/usage.js +46 -0
  195. package/src/routes/websocket.js +33 -0
  196. package/src/websocket/devWebSocket.js +78 -0
  197. package/vite.config.js +111 -0
  198. package/.eslintrc.yaml +0 -1
  199. package/lib/client/App.js +0 -80
  200. package/lib/client/Reload.js +0 -60
  201. package/lib/server/apiWrapper.js +0 -109
  202. package/lib/server/compileCss.js +0 -39
  203. package/manager/processes/compileCss.mjs +0 -54
  204. package/manager/processes/nextBuild.mjs +0 -50
  205. package/next.config.js +0 -25
  206. package/pages/_document.js +0 -109
  207. package/pages/api/agent/[...path].js +0 -86
  208. package/pages/api/auth/[...nextauth].js +0 -48
  209. package/pages/api/client-error.js +0 -45
  210. package/pages/api/js/[env].js +0 -42
  211. package/pages/api/page/[...pageId].js +0 -74
  212. package/pages/api/reload.js +0 -53
  213. package/pages/api/request/[...path].js +0 -41
  214. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  215. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  216. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  217. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  218. /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
+ };