stitch2 0.24.2 → 0.24.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/canvas.html CHANGED
@@ -654,6 +654,33 @@ function measure(f) {
654
654
  }
655
655
 
656
656
  const minHeight = (s) => (bigFrame(s) ? 800 : 844)
657
+ /** Whether two addresses are the same page: the same place, whatever their query or hash say. */
658
+ function samePage(a, b) {
659
+ try { const x = new URL(a), y = new URL(b); return x.origin === y.origin && x.pathname === y.pathname } catch { return false }
660
+ }
661
+ /**
662
+ * A page keeps growing after it loads: a table fetches its rows, an image arrives, a font swaps in. The measurements at load and in the first
663
+ * seconds catch what is there by then; this watches the page itself, so the frame is as tall as its page whenever the page changes size. The
664
+ * observer belongs to the loaded document and is replaced when the page reloads.
665
+ */
666
+ function watchSize(f, iframe) {
667
+ f.sizeWatch?.disconnect()
668
+ f.sizeWatch = null
669
+ try {
670
+ const doc = iframe.contentDocument
671
+ const Observer = iframe.contentWindow?.ResizeObserver
672
+ if (!doc || !Observer) return
673
+ let queued
674
+ const watch = new Observer(() => {
675
+ clearTimeout(queued)
676
+ // Wait for the page to settle: a table draws many rows in a few frames, and one measure after them is enough.
677
+ queued = setTimeout(() => { if (f.sizeWatch === watch && f.iframe === iframe) measure(f) }, 80)
678
+ })
679
+ watch.observe(doc.documentElement)
680
+ if (doc.body) watch.observe(doc.body)
681
+ f.sizeWatch = watch
682
+ } catch {}
683
+ }
657
684
  function setHeight(f, h) {
658
685
  h = Math.min(MAX_FRAME_HEIGHT, Math.max(minHeight(f.screen), h))
659
686
  HEIGHTS[`${f.screen.path}@${f.screen.width}`] = h
@@ -677,6 +704,10 @@ const frameSrc = (s) => `/${s.path}?pick&canvas=${s.viewportHeight ?? 844}${lang
677
704
  // The rest show the picture the server renders (/api/shot). Without pictures (no Chromium), frames in view stay
678
705
  // live at any zoom, still at most MAX_LIVE. ----
679
706
  const LIVE_ZOOM = 0.3, MAX_LIVE = 6, SHOTS_AT_ONCE = 3
707
+ // A picture costs the server a whole page render. A frame narrower than MIN_SHOT_PX on screen shows nothing you could read, so it is not
708
+ // asked for (fit-all with hundreds of screens would otherwise queue a render of every one, and again after every shared file or link change),
709
+ // and at most SHOTS_QUEUED pictures wait at a time, the nearest to the middle first; as they arrive the next ones are asked for.
710
+ const MIN_SHOT_PX = 64, SHOTS_QUEUED = 24
680
711
  let shotRev = 0, shotErrors = 0, syncTimer
681
712
  const shotsFailed = () => shotErrors > 2
682
713
  const scheduleSync = () => { clearTimeout(syncTimer); syncTimer = setTimeout(syncLive, 120) }
@@ -708,7 +739,9 @@ function mount(f) {
708
739
  // `load` also fires for the initial about:blank document. Compare the
709
740
  // loaded document itself with the current iframe target (which may have
710
741
  // changed for a language/theme reload while this mount was pending).
711
- return f.iframe === iframe && iframe.contentDocument === doc && doc.readyState === 'complete' && doc.location.href === iframe.src
742
+ // The address is compared without its query: a page may rewrite its own
743
+ // (a table that keeps its state there), and that is still the same page.
744
+ return f.iframe === iframe && iframe.contentDocument === doc && doc.readyState === 'complete' && samePage(doc.location.href, iframe.src)
712
745
  }
713
746
  iframe.addEventListener('load', () => {
714
747
  if (f.iframe !== iframe) return
@@ -727,6 +760,7 @@ function mount(f) {
727
760
  }, 350)
728
761
  setTimeout(() => { if (f.iframe === iframe) measure(f) }, 1200)
729
762
  setTimeout(() => { if (f.iframe === iframe) measure(f) }, 2500)
763
+ watchSize(f, iframe)
730
764
  })()
731
765
  })
732
766
  iframe.src = frameSrc(f.screen)
@@ -735,6 +769,8 @@ function mount(f) {
735
769
  function unmount(f) {
736
770
  if (!f.iframe) return
737
771
  clearTimeout(f.reveal)
772
+ f.sizeWatch?.disconnect()
773
+ f.sizeWatch = null
738
774
  // Cleared first: a page still loading can fire its load event as it's removed.
739
775
  const iframe = f.iframe
740
776
  f.iframe = null
@@ -787,7 +823,11 @@ function syncLive() {
787
823
  // Keep the page until its picture is loaded; switching zoom must never reveal an empty frame.
788
824
  for (const f of frames.values()) if (f.iframe && !live.has(f) && (!visible.has(f) || (f.img.complete && f.img.naturalWidth))) unmount(f)
789
825
  for (const f of live) mount(f)
790
- shotQueue = near.map(({ f }) => [f, shotUrl(f.screen)])
826
+ shotQueue = near
827
+ .filter(({ f }) => f.screen.width * view.z >= MIN_SHOT_PX)
828
+ .map(({ f }) => [f, shotUrl(f.screen)])
829
+ .filter(([f, url]) => f.img.getAttribute('src') !== url)
830
+ .slice(0, SHOTS_QUEUED)
791
831
  pumpShots()
792
832
  }
793
833
 
@@ -1168,12 +1208,14 @@ events.onmessage = async (e) => {
1168
1208
  // (tokens.tailwind.css), the components.
1169
1209
  const shared = paths.some((p) => /\.(css|js|mjs)$/.test(p) || p.startsWith('components/'))
1170
1210
  if (shared) shotRev++
1171
- for (const f of frames.values()) {
1172
- if (shared || html.includes(f.screen.path)) {
1173
- f.iframe?.contentWindow?.location.reload()
1174
- f.el.classList.remove('changed'); void f.el.offsetWidth; f.el.classList.add('changed')
1175
- }
1211
+ // One reflow for all the frames that flash, not one each: approving a version can touch every screen that linked to it.
1212
+ const touched = [...frames.values()].filter((f) => shared || html.includes(f.screen.path))
1213
+ for (const f of touched) {
1214
+ f.iframe?.contentWindow?.location.reload()
1215
+ f.el.classList.remove('changed')
1176
1216
  }
1217
+ void viewport.offsetWidth
1218
+ for (const f of touched) f.el.classList.add('changed')
1177
1219
  scheduleSync()
1178
1220
  }
1179
1221
 
package/dist/shots.js CHANGED
@@ -31,8 +31,13 @@ function sharedStamp() {
31
31
  if (depth < 4)
32
32
  walk(join(dir, e.name), depth + 1);
33
33
  }
34
- else if (!e.name.endsWith('.html') && !(depth === 0 && REPORTS.has(e.name)))
35
- newest = Math.max(newest, statSync(join(dir, e.name)).mtimeMs);
34
+ else if (!e.name.endsWith('.html') && !(depth === 0 && REPORTS.has(e.name))) {
35
+ // A link to nothing (a symlinked folder that is not there on this machine) is not a file that can change a picture.
36
+ try {
37
+ newest = Math.max(newest, statSync(join(dir, e.name)).mtimeMs);
38
+ }
39
+ catch { }
40
+ }
36
41
  }
37
42
  };
38
43
  walk(ROOT, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "A design canvas, layout and type checks, component consistency and screen versioning for agent-built UI, on Google's DESIGN.md format.",
5
5
  "license": "MIT",
6
6
  "author": "ifokeev",