@widgetic/creator 0.3.64 → 0.3.66
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/README.md +15 -0
- package/dist/CreatorApp.svelte +650 -176
- package/dist/CreatorApp.svelte.d.ts +3 -1
- package/dist/components/VersionDropdownRow.svelte +5 -3
- package/dist/components/WidgetDetails.svelte +34 -14
- package/dist/components/WidgetDetails.svelte.d.ts +3 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +114 -36
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +4 -0
- package/dist/utils/freezeWatchdog.d.ts +38 -0
- package/dist/utils/freezeWatchdog.js +161 -0
- package/dist/utils/persistWidgetThumbnail.d.ts +12 -1
- package/dist/utils/persistWidgetThumbnail.js +14 -7
- package/dist/utils/pinScrollToBottom.d.ts +12 -0
- package/dist/utils/pinScrollToBottom.js +43 -0
- package/package.json +4 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread responsiveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* PerformanceObserver('longtask') only fires AFTER a blocking task completes —
|
|
5
|
+
* an infinite reactive loop, or heavy work running inside a same-origin iframe
|
|
6
|
+
* (html2canvas screenshot captures in the widget preview share the parent's main
|
|
7
|
+
* thread), is never attributed to the page. The result was a frozen site with no
|
|
8
|
+
* [FreezeReport] in the console.
|
|
9
|
+
*
|
|
10
|
+
* This watchdog flips the model: a Web Worker (own thread, unaffected by the
|
|
11
|
+
* blocked main thread) pings every 500ms and the main thread answers. When pongs
|
|
12
|
+
* stop arriving, the worker logs a [FreezeReport] straight to the page console
|
|
13
|
+
* (worker console output shows in DevTools) with the block duration and the last
|
|
14
|
+
* codegen status the main thread reported. It also reports recovery, so each
|
|
15
|
+
* freeze episode produces exactly two lines: block start + recovery.
|
|
16
|
+
*
|
|
17
|
+
* The duration alone never said *what* blocked, which is why the freeze survived
|
|
18
|
+
* several rounds of fixes. So the main thread also keeps breadcrumbs — long tasks
|
|
19
|
+
* seen by PerformanceObserver (with their iframe attribution, since a widget
|
|
20
|
+
* preview blocking the shared main thread looks identical to the page blocking
|
|
21
|
+
* itself) and explicit `measureSync` spans — and ships the ones it has not
|
|
22
|
+
* reported yet on the next pong. The pong that lands on recovery therefore
|
|
23
|
+
* carries the task that caused the freeze, named and measured.
|
|
24
|
+
*/
|
|
25
|
+
const WATCHDOG_SOURCE = `
|
|
26
|
+
let lastPongAt = performance.now();
|
|
27
|
+
let freezeStartedAt = null;
|
|
28
|
+
let reported = false;
|
|
29
|
+
let lastStatus = '';
|
|
30
|
+
let breadcrumbs = [];
|
|
31
|
+
const PING_MS = 500;
|
|
32
|
+
const STALL_MS = 4000;
|
|
33
|
+
|
|
34
|
+
function describeBreadcrumbs() {
|
|
35
|
+
if (breadcrumbs.length === 0) return 'no instrumented task over 200ms — the block is somewhere not yet measured';
|
|
36
|
+
return breadcrumbs
|
|
37
|
+
.map((b) => b.label + ' ' + Math.round(b.duration) + 'ms' + (b.source ? ' [' + b.source + ']' : ''))
|
|
38
|
+
.join(', ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setInterval(() => {
|
|
42
|
+
postMessage({ type: 'ping' });
|
|
43
|
+
const now = performance.now();
|
|
44
|
+
if (now - lastPongAt >= STALL_MS) {
|
|
45
|
+
if (!freezeStartedAt) freezeStartedAt = now - STALL_MS;
|
|
46
|
+
if (!reported) {
|
|
47
|
+
reported = true;
|
|
48
|
+
console.warn('[FreezeReport] Main thread unresponsive ' + Math.round(now - freezeStartedAt) +
|
|
49
|
+
'ms. Last status: "' + (lastStatus || '—') + '"');
|
|
50
|
+
}
|
|
51
|
+
} else if (freezeStartedAt) {
|
|
52
|
+
console.warn('[FreezeReport] Main thread recovered after ' + Math.round(now - freezeStartedAt) +
|
|
53
|
+
'ms. Last status: "' + (lastStatus || '—') + '". Blocking work: ' + describeBreadcrumbs());
|
|
54
|
+
freezeStartedAt = null;
|
|
55
|
+
reported = false;
|
|
56
|
+
}
|
|
57
|
+
breadcrumbs = [];
|
|
58
|
+
}, PING_MS);
|
|
59
|
+
onmessage = (e) => {
|
|
60
|
+
if (e.data && e.data.type === 'pong') {
|
|
61
|
+
lastPongAt = performance.now();
|
|
62
|
+
if (e.data.status) lastStatus = e.data.status;
|
|
63
|
+
if (e.data.breadcrumbs && e.data.breadcrumbs.length) {
|
|
64
|
+
breadcrumbs = breadcrumbs.concat(e.data.breadcrumbs).slice(-12);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
`;
|
|
69
|
+
/** Long tasks and spans below this are ordinary work, not a freeze. */
|
|
70
|
+
const BREADCRUMB_MIN_MS = 200;
|
|
71
|
+
/**
|
|
72
|
+
* Times a synchronous step and hands it to the watchdog when it blocks long
|
|
73
|
+
* enough to matter. Cheap enough to leave on any suspect path permanently: two
|
|
74
|
+
* `performance.now()` reads and, for ordinary fast calls, nothing else.
|
|
75
|
+
*/
|
|
76
|
+
export function measureSync(label, run) {
|
|
77
|
+
if (typeof performance === 'undefined')
|
|
78
|
+
return run();
|
|
79
|
+
const startedAt = performance.now();
|
|
80
|
+
try {
|
|
81
|
+
return run();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
const duration = performance.now() - startedAt;
|
|
85
|
+
if (duration >= BREADCRUMB_MIN_MS) {
|
|
86
|
+
const watchdog = globalThis.__widgeticFreezeWatchdog;
|
|
87
|
+
watchdog?.addBreadcrumb(label, duration);
|
|
88
|
+
console.warn(`[MainThreadBlock] ${label} took ${Math.round(duration)}ms`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Long tasks the page did not measure itself, including ones inside an iframe. */
|
|
93
|
+
function observeLongTasks(watchdog) {
|
|
94
|
+
if (typeof PerformanceObserver === 'undefined')
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const observer = new PerformanceObserver((list) => {
|
|
98
|
+
for (const entry of list.getEntries()) {
|
|
99
|
+
if (entry.duration < BREADCRUMB_MIN_MS)
|
|
100
|
+
continue;
|
|
101
|
+
// `containerSrc` is the one signal that separates the page blocking
|
|
102
|
+
// itself from a widget preview blocking the main thread it shares.
|
|
103
|
+
const attribution = entry.attribution?.[0];
|
|
104
|
+
const source = attribution?.containerSrc
|
|
105
|
+
|| attribution?.containerName
|
|
106
|
+
|| attribution?.containerType
|
|
107
|
+
|| undefined;
|
|
108
|
+
watchdog.addBreadcrumb('longtask', entry.duration, source);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
observer.observe({ entryTypes: ['longtask'] });
|
|
112
|
+
return () => observer.disconnect();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function installFreezeWatchdog() {
|
|
119
|
+
if (typeof window === 'undefined' || typeof Worker === 'undefined' || typeof Blob === 'undefined') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const existing = window.__widgeticFreezeWatchdog;
|
|
123
|
+
if (existing)
|
|
124
|
+
return existing;
|
|
125
|
+
const blob = new Blob([WATCHDOG_SOURCE], { type: 'text/javascript' });
|
|
126
|
+
const url = URL.createObjectURL(blob);
|
|
127
|
+
const worker = new Worker(url);
|
|
128
|
+
URL.revokeObjectURL(url);
|
|
129
|
+
let lastStatus = '';
|
|
130
|
+
let pendingBreadcrumbs = [];
|
|
131
|
+
worker.onmessage = (event) => {
|
|
132
|
+
if (event.data?.type === 'ping') {
|
|
133
|
+
worker.postMessage({ type: 'pong', status: lastStatus, breadcrumbs: pendingBreadcrumbs });
|
|
134
|
+
pendingBreadcrumbs = [];
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
let stopLongTaskObserver = null;
|
|
138
|
+
const watchdog = {
|
|
139
|
+
setStatus(status) {
|
|
140
|
+
lastStatus = status;
|
|
141
|
+
},
|
|
142
|
+
addBreadcrumb(label, duration, source) {
|
|
143
|
+
if (duration < BREADCRUMB_MIN_MS)
|
|
144
|
+
return;
|
|
145
|
+
pendingBreadcrumbs.push({ label, duration, source });
|
|
146
|
+
// Keep only the worst offenders: a freeze is one big task, not fifty.
|
|
147
|
+
if (pendingBreadcrumbs.length > 12) {
|
|
148
|
+
pendingBreadcrumbs.sort((a, b) => b.duration - a.duration);
|
|
149
|
+
pendingBreadcrumbs = pendingBreadcrumbs.slice(0, 12);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
dispose() {
|
|
153
|
+
stopLongTaskObserver?.();
|
|
154
|
+
worker.terminate();
|
|
155
|
+
delete window.__widgeticFreezeWatchdog;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
window.__widgeticFreezeWatchdog = watchdog;
|
|
159
|
+
stopLongTaskObserver = observeLongTasks(watchdog);
|
|
160
|
+
return watchdog;
|
|
161
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { WidgetsApi } from '@widgetic/api-sdk';
|
|
2
2
|
/**
|
|
3
|
-
* Upload a convert/preview screenshot to widgets.thumbnail_url via the API
|
|
3
|
+
* Upload a convert/preview screenshot to widgets.thumbnail_url via the API
|
|
4
|
+
* gateway.
|
|
5
|
+
*
|
|
6
|
+
* This used to be a hand-rolled fetch: the gateway route
|
|
7
|
+
* (`POST /widgets/{id}/thumbnail`) postdated the last SDK generation, so the
|
|
8
|
+
* module read `basePath` and the bearer token back out of the `WidgetsApi`
|
|
9
|
+
* instance's `Configuration` and hit the endpoint itself, unwrapping the
|
|
10
|
+
* response against four guessed shapes. The spec now carries the operation
|
|
11
|
+
* with a typed 200 (`UploadWidgetThumbnail200Response.data.thumbnailUrl`), the
|
|
12
|
+
* SDK was regenerated, and the fetch is retired — the typed method targets the
|
|
13
|
+
* same host the client was constructed with, which is what the Configuration
|
|
14
|
+
* read-back was approximating.
|
|
4
15
|
*/
|
|
5
16
|
export declare function persistWidgetThumbnail(widgetsClient: WidgetsApi | null | undefined, widgetId: string | null | undefined, imageDataUrl: string | null | undefined): Promise<string | null>;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Upload a convert/preview screenshot to widgets.thumbnail_url via the API
|
|
2
|
+
* Upload a convert/preview screenshot to widgets.thumbnail_url via the API
|
|
3
|
+
* gateway.
|
|
4
|
+
*
|
|
5
|
+
* This used to be a hand-rolled fetch: the gateway route
|
|
6
|
+
* (`POST /widgets/{id}/thumbnail`) postdated the last SDK generation, so the
|
|
7
|
+
* module read `basePath` and the bearer token back out of the `WidgetsApi`
|
|
8
|
+
* instance's `Configuration` and hit the endpoint itself, unwrapping the
|
|
9
|
+
* response against four guessed shapes. The spec now carries the operation
|
|
10
|
+
* with a typed 200 (`UploadWidgetThumbnail200Response.data.thumbnailUrl`), the
|
|
11
|
+
* SDK was regenerated, and the fetch is retired — the typed method targets the
|
|
12
|
+
* same host the client was constructed with, which is what the Configuration
|
|
13
|
+
* read-back was approximating.
|
|
3
14
|
*/
|
|
4
15
|
export async function persistWidgetThumbnail(widgetsClient, widgetId, imageDataUrl) {
|
|
5
16
|
if (!widgetsClient || !widgetId || typeof imageDataUrl !== 'string')
|
|
@@ -7,15 +18,11 @@ export async function persistWidgetThumbnail(widgetsClient, widgetId, imageDataU
|
|
|
7
18
|
if (!imageDataUrl.startsWith('data:image/'))
|
|
8
19
|
return null;
|
|
9
20
|
try {
|
|
10
|
-
const response = await widgetsClient.
|
|
21
|
+
const response = await widgetsClient.uploadWidgetThumbnail({
|
|
11
22
|
id: widgetId,
|
|
12
23
|
uploadWidgetThumbnailRequest: { imageDataUrl },
|
|
13
24
|
});
|
|
14
|
-
const
|
|
15
|
-
const thumbnailUrl = json?.data?.thumbnailUrl ||
|
|
16
|
-
json?.data?.thumbnail_url ||
|
|
17
|
-
json?.thumbnailUrl ||
|
|
18
|
-
null;
|
|
25
|
+
const thumbnailUrl = response.data?.thumbnailUrl ?? null;
|
|
19
26
|
if (thumbnailUrl) {
|
|
20
27
|
console.log('[persistWidgetThumbnail] stored', widgetId.slice(0, 8), thumbnailUrl.slice(0, 80));
|
|
21
28
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a scrollable container pinned to its bottom edge until the user scrolls
|
|
3
|
+
* up. Opening the widget-details panel on a small screen lays the panel out
|
|
4
|
+
* after mount, which can leave the outer scroll area (status bar + chat +
|
|
5
|
+
* footer) resting above its bottom — clipping the chat input. The observer
|
|
6
|
+
* re-pins to bottom whenever the content height or the box changes, so the
|
|
7
|
+
* input is always visible on open. Any manual upward scroll un-pins until the
|
|
8
|
+
* user returns to the bottom, preserving normal reading behavior.
|
|
9
|
+
*/
|
|
10
|
+
export declare function pinScrollToBottom(node: HTMLElement): {
|
|
11
|
+
destroy(): void;
|
|
12
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a scrollable container pinned to its bottom edge until the user scrolls
|
|
3
|
+
* up. Opening the widget-details panel on a small screen lays the panel out
|
|
4
|
+
* after mount, which can leave the outer scroll area (status bar + chat +
|
|
5
|
+
* footer) resting above its bottom — clipping the chat input. The observer
|
|
6
|
+
* re-pins to bottom whenever the content height or the box changes, so the
|
|
7
|
+
* input is always visible on open. Any manual upward scroll un-pins until the
|
|
8
|
+
* user returns to the bottom, preserving normal reading behavior.
|
|
9
|
+
*/
|
|
10
|
+
export function pinScrollToBottom(node) {
|
|
11
|
+
let pinned = true;
|
|
12
|
+
let rafId = null;
|
|
13
|
+
const scrollToBottom = () => {
|
|
14
|
+
if (!pinned)
|
|
15
|
+
return;
|
|
16
|
+
// rAF so the layout pass has settled before reading scrollHeight.
|
|
17
|
+
rafId = requestAnimationFrame(() => {
|
|
18
|
+
node.scrollTo({ top: node.scrollHeight, behavior: 'instant' });
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const onScroll = () => {
|
|
22
|
+
const atBottom = node.scrollTop + node.clientHeight >= node.scrollHeight - 8;
|
|
23
|
+
pinned = atBottom;
|
|
24
|
+
};
|
|
25
|
+
const observer = new ResizeObserver(scrollToBottom);
|
|
26
|
+
observer.observe(node);
|
|
27
|
+
// Content growing also needs re-pinning; observe children changes.
|
|
28
|
+
const contentObserver = new MutationObserver(scrollToBottom);
|
|
29
|
+
contentObserver.observe(node, { childList: true, subtree: true, characterData: true });
|
|
30
|
+
node.addEventListener('scroll', onScroll, { passive: true });
|
|
31
|
+
// Initial pin after the first layout pass.
|
|
32
|
+
const initialRaf = requestAnimationFrame(scrollToBottom);
|
|
33
|
+
return {
|
|
34
|
+
destroy() {
|
|
35
|
+
if (rafId !== null)
|
|
36
|
+
cancelAnimationFrame(rafId);
|
|
37
|
+
cancelAnimationFrame(initialRaf);
|
|
38
|
+
observer.disconnect();
|
|
39
|
+
contentObserver.disconnect();
|
|
40
|
+
node.removeEventListener('scroll', onScroll);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@widgetic/creator",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.66",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@sveltejs/kit": "^2.20.0",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dev-sv": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") VITE_BUILD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo local) NODE_ENV=development vite dev --port 5174",
|
|
46
46
|
"lps": "volta run npm link --save @widgetic/design-system && volta run npm link --save @widgetic/api-sdk && volta run npm link --save @widgetic/canvas",
|
|
47
47
|
"llinks": "ls -la node_modules/@widgetic",
|
|
48
|
+
"ldt": "volta run npm link --save @widgetic/dev-tools",
|
|
48
49
|
"clean-npm-cache": "npm cache clean --force",
|
|
49
50
|
"clean-svelte-cache": "rm -rf .svelte-kit && npx svelte-kit sync",
|
|
50
51
|
"clean-vite-cache": "rm -rf node_modules/.vite",
|
|
@@ -67,12 +68,13 @@
|
|
|
67
68
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
68
69
|
"@tailwindcss/postcss": "^4.1.4",
|
|
69
70
|
"@tailwindcss/typography": "^0.5.15",
|
|
70
|
-
"@types/
|
|
71
|
+
"@types/node": "26.4.0",
|
|
71
72
|
"@widgetic/api-sdk": "^1.0.10",
|
|
72
73
|
"@widgetic/cache-layer": "^0.1.2",
|
|
73
74
|
"@widgetic/canvas": "^0.5.4",
|
|
74
75
|
"@widgetic/chat": "^0.1.4",
|
|
75
76
|
"@widgetic/design-system": "^0.5.8",
|
|
77
|
+
"@widgetic/dev-tools": "^0.2.0",
|
|
76
78
|
"@widgetic/editor": "^4.0.1",
|
|
77
79
|
"@widgetic/file-browser": "^0.5.17",
|
|
78
80
|
"autoprefixer": "^10.4.20",
|