@khanglvm/relay 0.2.0 → 0.3.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/src/ui/blocks.js CHANGED
@@ -371,6 +371,20 @@
371
371
  return mermaidPromise;
372
372
  }
373
373
 
374
+ let vizPromise = null;
375
+ function loadViz() {
376
+ if (window.Viz) return Promise.resolve(window.Viz);
377
+ if (vizPromise) return vizPromise;
378
+ vizPromise = new Promise((resolve, reject) => {
379
+ const s = document.createElement('script');
380
+ s.src = '/vendor/viz-standalone.js';
381
+ s.onload = () => (window.Viz ? resolve(window.Viz) : reject(new Error('Graphviz failed to load')));
382
+ s.onerror = () => reject(new Error('Graphviz failed to load'));
383
+ document.head.appendChild(s);
384
+ });
385
+ return vizPromise;
386
+ }
387
+
374
388
  // ---------- chart ----------
375
389
  function clampHeight(h, def) {
376
390
  const n = Number(h);
@@ -648,6 +662,120 @@
648
662
  );
649
663
  }
650
664
 
665
+ // ---------- graphviz (offline, vendored Viz.js -> SVG) ----------
666
+ // Same sizing rule as mermaid: never upscale past the diagram's natural
667
+ // width; shrink on narrow screens. Authors set their own colors so there is
668
+ // no theme re-render.
669
+ function sizeDiagramSvg(svgEl) {
670
+ if (!svgEl) return;
671
+ svgEl.removeAttribute('height');
672
+ svgEl.removeAttribute('width');
673
+ const vb = svgEl.viewBox && svgEl.viewBox.baseVal;
674
+ if (vb && vb.width > 0) {
675
+ svgEl.style.width = '100%';
676
+ svgEl.style.maxWidth = Math.ceil(vb.width) + 'px';
677
+ svgEl.style.height = 'auto';
678
+ } else {
679
+ svgEl.style.maxWidth = '100%';
680
+ }
681
+ }
682
+
683
+ function renderGraphviz(block, ctx, blockId) {
684
+ const container = el('div', { class: 'blk-graphviz' });
685
+ loadViz()
686
+ .then((Viz) => Viz.instance())
687
+ .then((viz) => {
688
+ const svgEl = viz.renderSVGElement(block.dot || '');
689
+ sizeDiagramSvg(svgEl);
690
+ container.replaceChildren(svgEl);
691
+ if (!ctx.annotate) return;
692
+ const parts = svgEl.querySelectorAll('g.node, g.edge');
693
+ parts.forEach((g) => {
694
+ const titleEl = g.querySelector('title');
695
+ const nodeId = (g.id || (titleEl && titleEl.textContent) || '').trim();
696
+ // Label text lives in <text> elements; g.textContent would also
697
+ // include the <title> child and duplicate the label.
698
+ const labels = Array.from(g.querySelectorAll('text')).map((t) => t.textContent.trim()).filter(Boolean);
699
+ const text = (labels.join(' ') || (titleEl && titleEl.textContent) || '').trim().slice(0, 120);
700
+ ctx.annotate.register(g, {
701
+ blockId,
702
+ questionId: ctx.questionId,
703
+ target: {
704
+ kind: 'graphviz-node',
705
+ nodeId,
706
+ text,
707
+ },
708
+ });
709
+ });
710
+ })
711
+ .catch((err) => {
712
+ container.replaceChildren(
713
+ el('div', { class: 'blk-error' }, 'Graphviz error: ' + (err && err.message ? err.message : String(err)))
714
+ );
715
+ });
716
+ return container;
717
+ }
718
+
719
+ // ---------- plantuml (server-rendered; client deflate-raw + base64 variant) ----------
720
+ const PLANTUML_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
721
+
722
+ // PlantUML's base64 variant: 3 bytes -> 4 chars using PLANTUML_ALPHABET.
723
+ function encode64(bytes) {
724
+ let out = '';
725
+ for (let i = 0; i < bytes.length; i += 3) {
726
+ const b1 = bytes[i];
727
+ const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
728
+ const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
729
+ out += PLANTUML_ALPHABET[b1 >> 2];
730
+ out += PLANTUML_ALPHABET[((b1 & 0x3) << 4) | (b2 >> 4)];
731
+ if (i + 1 < bytes.length) out += PLANTUML_ALPHABET[((b2 & 0xf) << 2) | (b3 >> 6)];
732
+ if (i + 2 < bytes.length) out += PLANTUML_ALPHABET[b3 & 0x3f];
733
+ }
734
+ return out;
735
+ }
736
+
737
+ async function encodePlantUml(code) {
738
+ if (typeof CompressionStream === 'undefined') {
739
+ throw new Error('CompressionStream unavailable');
740
+ }
741
+ const bytes = new TextEncoder().encode(String(code));
742
+ const blob = new Blob([bytes]);
743
+ const compressed = await new Response(
744
+ blob.stream().pipeThrough(new CompressionStream('deflate-raw'))
745
+ ).arrayBuffer();
746
+ return encode64(new Uint8Array(compressed));
747
+ }
748
+
749
+ function renderPlantuml(block, ctx, blockId) {
750
+ const container = el('div', { class: 'blk-plantuml' });
751
+ const fail = () =>
752
+ container.replaceChildren(
753
+ el('div', { class: 'blk-error' }, 'PlantUML needs network access and a modern browser')
754
+ );
755
+ encodePlantUml(block.code || '')
756
+ .then((encoded) => {
757
+ const server = block.server || 'https://www.plantuml.com/plantuml';
758
+ const img = el('img', {
759
+ class: 'blk-plantuml-img',
760
+ src: server + '/svg/' + encoded,
761
+ alt: 'PlantUML diagram',
762
+ loading: 'lazy',
763
+ });
764
+ if (block.height) img.style.height = clampHeight(block.height, 360) + 'px';
765
+ img.addEventListener('error', fail);
766
+ container.replaceChildren(img);
767
+ if (ctx.annotate) {
768
+ ctx.annotate.register(img, {
769
+ blockId,
770
+ questionId: ctx.questionId,
771
+ target: { kind: 'image', label: 'PlantUML diagram' },
772
+ });
773
+ }
774
+ })
775
+ .catch(fail);
776
+ return container;
777
+ }
778
+
651
779
  // ---------- html (sandboxed iframe) ----------
652
780
  function renderHtml(block, ctx, blockId) {
653
781
  const height = clampHeight(block.height, 360);
@@ -690,6 +818,14 @@
690
818
  inner = renderMermaid(block, ctx, blockId);
691
819
  wrapper.append(inner);
692
820
  break;
821
+ case 'graphviz':
822
+ inner = renderGraphviz(block, ctx, blockId);
823
+ wrapper.append(inner);
824
+ break;
825
+ case 'plantuml':
826
+ inner = renderPlantuml(block, ctx, blockId);
827
+ wrapper.append(inner);
828
+ break;
693
829
  case 'html':
694
830
  inner = renderHtml(block, ctx, blockId);
695
831
  wrapper.append(inner);
package/src/ui/style.css CHANGED
@@ -206,6 +206,15 @@ textarea { min-height: 90px; resize: vertical; }
206
206
  z-index: 10; display: none;
207
207
  }
208
208
 
209
+ /* Live-update toast: flagged after a `rly update` reload (see app.js).
210
+ Fixed top-center, accent-soft bg, accent text; JS auto-removes it. */
211
+ .toast {
212
+ position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
213
+ background: var(--accent-soft); color: var(--accent);
214
+ padding: 10px 18px; border-radius: 10px; font-size: 0.9rem;
215
+ box-shadow: var(--shadow-lift); z-index: 50;
216
+ }
217
+
209
218
  .done { text-align: center; padding: 80px 18px; }
210
219
  .done .mark {
211
220
  width: 64px; height: 64px; border-radius: 50%;
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "chart.js": "4.5.1",
3
3
  "mermaid": "11.15.0",
4
- "updatedAt": "2026-06-11T09:20:33.586Z"
4
+ "@viz-js/viz": "3.28.0",
5
+ "updatedAt": "2026-06-11T10:09:03.988Z"
5
6
  }