opencode-generative-ui 0.1.3 → 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 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 isSVG = shell?.dataset.kind === 'svg';
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
- stage.style.transform = 'translate(' + tx + 'px, ' + ty + 'px) scale(' + scale + ')';
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 (isSVG) {
523
+ if (usesNativeSVGZoom()) {
461
524
  fitView();
462
525
  return;
463
526
  }
@@ -541,6 +604,12 @@ window._runScripts = function() {
541
604
  }
542
605
  old.parentNode.replaceChild(s, old);
543
606
  });
607
+ var refresh = function() {
608
+ if (window._ocRefresh) window._ocRefresh();
609
+ };
610
+ requestAnimationFrame(refresh);
611
+ setTimeout(refresh, 0);
612
+ setTimeout(refresh, 60);
544
613
  };
545
614
  window._setContent('${escapedCode}');
546
615
  window._runScripts();
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-generative-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "OpenCode plugin that renders HTML and SVG widgets in native macOS windows using Glimpse.",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",