opencode-generative-ui 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +78 -8
- package/dist/lib/guidelines.js +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -178,7 +178,7 @@ const WIDGET_SHELL_SCRIPT = String.raw `(() => {
|
|
|
178
178
|
const zoomOutButton = document.getElementById('oc-zoom-out');
|
|
179
179
|
const fitButton = document.getElementById('oc-fit');
|
|
180
180
|
const resetButton = document.getElementById('oc-reset');
|
|
181
|
-
const
|
|
181
|
+
const declaredSVG = shell?.dataset.kind === 'svg';
|
|
182
182
|
|
|
183
183
|
if (!shell || !viewport || !stage || !content || !zoomValue) return;
|
|
184
184
|
|
|
@@ -197,6 +197,9 @@ const WIDGET_SHELL_SCRIPT = String.raw `(() => {
|
|
|
197
197
|
let panStartTy = 0;
|
|
198
198
|
let gestureScaleStart = null;
|
|
199
199
|
let gestureAnchor = null;
|
|
200
|
+
let svgEl = null;
|
|
201
|
+
let svgBaseWidth = 0;
|
|
202
|
+
let svgBaseHeight = 0;
|
|
200
203
|
|
|
201
204
|
const isEditableTarget = (target) => {
|
|
202
205
|
if (!(target instanceof Element)) return false;
|
|
@@ -205,7 +208,57 @@ const WIDGET_SHELL_SCRIPT = String.raw `(() => {
|
|
|
205
208
|
|
|
206
209
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
207
210
|
|
|
211
|
+
const syncSVG = () => {
|
|
212
|
+
const candidate = content.querySelector('svg');
|
|
213
|
+
if (!(candidate instanceof SVGSVGElement)) {
|
|
214
|
+
svgEl = null;
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (svgEl !== candidate) {
|
|
219
|
+
svgEl = candidate;
|
|
220
|
+
const viewBox = svgEl.viewBox?.baseVal;
|
|
221
|
+
let width = viewBox && viewBox.width > 0 ? viewBox.width : Number.parseFloat(svgEl.getAttribute('width') || '');
|
|
222
|
+
let height = viewBox && viewBox.height > 0 ? viewBox.height : Number.parseFloat(svgEl.getAttribute('height') || '');
|
|
223
|
+
|
|
224
|
+
if (!(width > 0) || !(height > 0)) {
|
|
225
|
+
const rect = svgEl.getBoundingClientRect();
|
|
226
|
+
width = rect.width;
|
|
227
|
+
height = rect.height;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!(width > 0) || !(height > 0)) {
|
|
231
|
+
width = 680;
|
|
232
|
+
height = 420;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
svgBaseWidth = width;
|
|
236
|
+
svgBaseHeight = height;
|
|
237
|
+
svgEl.style.display = 'block';
|
|
238
|
+
svgEl.style.maxWidth = 'none';
|
|
239
|
+
svgEl.style.maxHeight = 'none';
|
|
240
|
+
if (!svgEl.getAttribute('preserveAspectRatio')) {
|
|
241
|
+
svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return true;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const usesNativeSVGZoom = () => {
|
|
249
|
+
if (!syncSVG()) return false;
|
|
250
|
+
if (declaredSVG) return true;
|
|
251
|
+
return !content.querySelector('input, button, select, textarea, [contenteditable="true"], [contenteditable=""]');
|
|
252
|
+
};
|
|
253
|
+
|
|
208
254
|
const measureContent = () => {
|
|
255
|
+
if (usesNativeSVGZoom()) {
|
|
256
|
+
return {
|
|
257
|
+
width: Math.max(svgBaseWidth, 1),
|
|
258
|
+
height: Math.max(svgBaseHeight, 1),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
209
262
|
const width = Math.max(content.scrollWidth, content.offsetWidth, content.clientWidth);
|
|
210
263
|
const height = Math.max(content.scrollHeight, content.offsetHeight, content.clientHeight);
|
|
211
264
|
return {
|
|
@@ -219,7 +272,17 @@ const WIDGET_SHELL_SCRIPT = String.raw `(() => {
|
|
|
219
272
|
};
|
|
220
273
|
|
|
221
274
|
const render = () => {
|
|
222
|
-
|
|
275
|
+
if (usesNativeSVGZoom() && svgEl) {
|
|
276
|
+
stage.style.transform = 'translate(' + tx + 'px, ' + ty + 'px)';
|
|
277
|
+
svgEl.style.width = svgBaseWidth * scale + 'px';
|
|
278
|
+
svgEl.style.height = svgBaseHeight * scale + 'px';
|
|
279
|
+
} else {
|
|
280
|
+
if (svgEl) {
|
|
281
|
+
svgEl.style.width = '';
|
|
282
|
+
svgEl.style.height = '';
|
|
283
|
+
}
|
|
284
|
+
stage.style.transform = 'translate(' + tx + 'px, ' + ty + 'px) scale(' + scale + ')';
|
|
285
|
+
}
|
|
223
286
|
updateZoomLabel();
|
|
224
287
|
};
|
|
225
288
|
|
|
@@ -457,7 +520,7 @@ const WIDGET_SHELL_SCRIPT = String.raw `(() => {
|
|
|
457
520
|
const refreshLayout = () => {
|
|
458
521
|
if (!hasInitialView) {
|
|
459
522
|
hasInitialView = true;
|
|
460
|
-
if (
|
|
523
|
+
if (usesNativeSVGZoom()) {
|
|
461
524
|
fitView();
|
|
462
525
|
return;
|
|
463
526
|
}
|
|
@@ -478,12 +541,13 @@ function escapeJS(s) {
|
|
|
478
541
|
.replace(/\r/g, "\\r")
|
|
479
542
|
.replace(/<\/script>/gi, "<\\/script>");
|
|
480
543
|
}
|
|
481
|
-
function shellHTML(isSVG = false) {
|
|
544
|
+
function shellHTML(code, isSVG = false) {
|
|
482
545
|
const bodyClass = isSVG ? "oc-svg" : "oc-html";
|
|
483
546
|
const bodyKind = isSVG ? "svg" : "html";
|
|
484
547
|
const hint = isSVG
|
|
485
548
|
? "Pinch or Cmd/Ctrl + wheel to zoom. Pan with middle-drag, Space + drag, or arrow keys."
|
|
486
549
|
: "Zoom with pinch or Cmd/Ctrl + wheel. Pan with middle-drag, Space + drag, or arrow keys without breaking widget controls.";
|
|
550
|
+
const escapedCode = escapeJS(code);
|
|
487
551
|
return `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
488
552
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
489
553
|
<style>
|
|
@@ -540,7 +604,15 @@ window._runScripts = function() {
|
|
|
540
604
|
}
|
|
541
605
|
old.parentNode.replaceChild(s, old);
|
|
542
606
|
});
|
|
607
|
+
var refresh = function() {
|
|
608
|
+
if (window._ocRefresh) window._ocRefresh();
|
|
609
|
+
};
|
|
610
|
+
requestAnimationFrame(refresh);
|
|
611
|
+
setTimeout(refresh, 0);
|
|
612
|
+
setTimeout(refresh, 60);
|
|
543
613
|
};
|
|
614
|
+
window._setContent('${escapedCode}');
|
|
615
|
+
window._runScripts();
|
|
544
616
|
</script>
|
|
545
617
|
<script>${WIDGET_SHELL_SCRIPT}</script>
|
|
546
618
|
</body></html>`;
|
|
@@ -616,7 +688,7 @@ const showWidget = tool({
|
|
|
616
688
|
});
|
|
617
689
|
let win;
|
|
618
690
|
try {
|
|
619
|
-
win = open(shellHTML(isSVG), {
|
|
691
|
+
win = open(shellHTML(code, isSVG), {
|
|
620
692
|
width,
|
|
621
693
|
height,
|
|
622
694
|
title,
|
|
@@ -640,9 +712,6 @@ const showWidget = tool({
|
|
|
640
712
|
}
|
|
641
713
|
resolve(`Widget \"${title}\" rendered and shown to the user (${width}x${height}). ${reason}`);
|
|
642
714
|
};
|
|
643
|
-
win.on("ready", () => {
|
|
644
|
-
win.send(`window._setContent('${escapeJS(code)}'); window._runScripts();`);
|
|
645
|
-
});
|
|
646
715
|
win.on("message", (data) => {
|
|
647
716
|
messageData = data;
|
|
648
717
|
finish("User sent data from widget.");
|
|
@@ -673,6 +742,7 @@ const SYSTEM_GUIDANCE = [
|
|
|
673
742
|
"Always call visualize_read_me once before the first show_widget call in a session, then set i_have_seen_read_me: true.",
|
|
674
743
|
"For HTML widgets, provide a fragment only. Do not include DOCTYPE, html, head, or body tags.",
|
|
675
744
|
"For SVG widgets, start widget_code with <svg>.",
|
|
745
|
+
"If the user may zoom in or inspect labels closely, prefer pure SVG or HTML that renders to a single inline SVG. Avoid canvas unless the interaction genuinely needs it.",
|
|
676
746
|
"Keep widgets focused and appropriately sized. Default size is 800x600 unless the content needs another size.",
|
|
677
747
|
].join("\n");
|
|
678
748
|
let registeredExitHandler = false;
|
package/dist/lib/guidelines.js
CHANGED
|
@@ -29,6 +29,7 @@ These rules apply to ALL use cases.
|
|
|
29
29
|
- **Seamless**: Users shouldn't notice where claude.ai ends and your widget begins.
|
|
30
30
|
- **Flat**: No gradients, mesh backgrounds, noise textures, or decorative effects. Clean flat surfaces.
|
|
31
31
|
- **Compact**: Show the essential inline. Explain the rest in text.
|
|
32
|
+
- **Scalable when inspected**: If the user is likely to zoom in or inspect labels closely (ERDs, architecture diagrams, charts with dense labels), prefer pure SVG or HTML that renders to a single inline SVG. Avoid canvas unless you need true pixel drawing or Chart.js-level interaction.
|
|
32
33
|
- **Text goes in your response, visuals go in the tool** — All explanatory text, descriptions, introductions, and summaries must be written as normal response text OUTSIDE the tool call. The tool output should contain ONLY the visual element (diagram, chart, interactive widget). Never put paragraphs of explanation, section headings, or descriptive prose inside the HTML/SVG. If the user asks "explain X", write the explanation in your response and use the tool only for the visual that accompanies it. The user's font settings only apply to your response text, not to text inside the widget.
|
|
33
34
|
|
|
34
35
|
### Streaming
|
|
@@ -118,6 +119,7 @@ const CHARTS_CHART_JS = `## Charts (Chart.js)
|
|
|
118
119
|
\`\`\`
|
|
119
120
|
|
|
120
121
|
**Chart.js rules**:
|
|
122
|
+
- Prefer SVG for charts the user may inspect closely or zoom heavily. Use Chart.js when live interaction, animation, or built-in chart behaviors matter more than zoom fidelity.
|
|
121
123
|
- Canvas cannot resolve CSS variables. Use hardcoded hex or Chart.js defaults.
|
|
122
124
|
- Wrap \`<canvas>\` in \`<div>\` with explicit \`height\` and \`position: relative\`.
|
|
123
125
|
- **Canvas sizing**: set height ONLY on the wrapper div, never on the canvas element itself. Use position: relative on the wrapper and responsive: true, maintainAspectRatio: false in Chart.js options. Never set CSS height directly on canvas — this causes wrong dimensions, especially for horizontal bar charts.
|
package/package.json
CHANGED