foss-design 0.5.0 → 0.6.1

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.
@@ -467,6 +467,77 @@ var FrameInspector = class {
467
467
  };
468
468
  var SWALLOWED = ["pointerdown", "mousedown", "pointerup", "mouseup", "dblclick", "submit"];
469
469
 
470
+ // src/runtime/public.ts
471
+ var ATTRS = ["src", "srcset", "poster", "href", "xlink:href"];
472
+ var LOADS_HREF = /* @__PURE__ */ new Set(["link", "image", "use", "feimage"]);
473
+ function servePublicFolder(folder) {
474
+ if (!folder?.files.length) return;
475
+ const files = new Set(folder.files);
476
+ const base = new URL(folder.base, location.href);
477
+ const resolve = (value) => {
478
+ if (!value.startsWith("/") || value.startsWith("//")) return null;
479
+ const [, pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value) ?? [];
480
+ let rel;
481
+ try {
482
+ rel = decodeURIComponent(pathname.slice(1));
483
+ } catch {
484
+ return null;
485
+ }
486
+ return files.has(rel) ? new URL(pathname.slice(1), base).href + suffix : null;
487
+ };
488
+ const rewrite = (name, value) => {
489
+ if (name !== "srcset") return resolve(value);
490
+ let changed = false;
491
+ const next = value.split(",").map((candidate) => {
492
+ const [url = "", ...rest] = candidate.trim().split(/\s+/);
493
+ const resolved = resolve(url);
494
+ if (!resolved) return candidate;
495
+ changed = true;
496
+ return [resolved, ...rest].join(" ");
497
+ }).join(", ");
498
+ return changed ? next : null;
499
+ };
500
+ const applies = (element, name) => name.endsWith("href") ? LOADS_HREF.has(element.localName.toLowerCase()) : true;
501
+ const setAttribute = Element.prototype.setAttribute;
502
+ Element.prototype.setAttribute = function(name, value) {
503
+ const lower = name.toLowerCase();
504
+ const next = ATTRS.includes(lower) && applies(this, lower) ? rewrite(lower, String(value)) : null;
505
+ setAttribute.call(this, name, next ?? value);
506
+ };
507
+ const fix = (element) => {
508
+ for (const name of ATTRS) {
509
+ const value = element.getAttribute(name);
510
+ if (value === null || !applies(element, name)) continue;
511
+ const next = rewrite(name, value);
512
+ if (next !== null) setAttribute.call(element, name, next);
513
+ }
514
+ };
515
+ const fixTree = (root) => {
516
+ fix(root);
517
+ for (const element of root.querySelectorAll("[src], [srcset], [poster], [href]")) fix(element);
518
+ };
519
+ new MutationObserver((records) => {
520
+ for (const record of records) {
521
+ if (record.type === "attributes") fix(record.target);
522
+ for (const node of record.addedNodes) if (node.nodeType === Node.ELEMENT_NODE) fixTree(node);
523
+ }
524
+ }).observe(document, {
525
+ subtree: true,
526
+ childList: true,
527
+ attributes: true,
528
+ attributeFilter: ["src", "srcset", "poster", "href"]
529
+ });
530
+ if (document.documentElement) fixTree(document.documentElement);
531
+ const fetch2 = window.fetch.bind(window);
532
+ window.fetch = (input, init) => {
533
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : null;
534
+ let path = raw;
535
+ if (raw && input instanceof URL && input.origin === location.origin) path = input.pathname + input.search;
536
+ const resolved = path ? resolve(path) : null;
537
+ return fetch2(resolved ?? input, init);
538
+ };
539
+ }
540
+
470
541
  // src/runtime/typography.tsx
471
542
  import { useEffect, useSyncExternalStore } from "react";
472
543
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -566,6 +637,8 @@ var config = window.__DESIGN__ ?? { key: "", canvas: "", id: "", props: {} };
566
637
  var embedded = window.parent !== window;
567
638
  var query = new URLSearchParams(window.location.search);
568
639
  var capture = query.has("capture");
640
+ var frameUrl = location.pathname + location.search + location.hash;
641
+ servePublicFolder(config.public);
569
642
  var theme = config.theme ?? (query.get("theme") === "dark" ? "dark" : "light");
570
643
  var themeListeners = /* @__PURE__ */ new Set();
571
644
  function applyTheme(next) {
@@ -669,9 +742,14 @@ function watchSize() {
669
742
  window.addEventListener("load", queue);
670
743
  queue();
671
744
  }
745
+ var touched = false;
746
+ var inspecting = false;
747
+ var loaded = false;
748
+ var signalled = false;
749
+ var holds = 0;
672
750
  var snapshotTimer;
673
751
  function scheduleSnapshot() {
674
- if (!config.snapshots || !embedded || capture) return;
752
+ if (!config.snapshots || !embedded || capture || touched || !signalled) return;
675
753
  clearTimeout(snapshotTimer);
676
754
  snapshotTimer = setTimeout(() => {
677
755
  const run = () => void takeSnapshot();
@@ -680,6 +758,7 @@ function scheduleSnapshot() {
680
758
  }, 1800);
681
759
  }
682
760
  async function takeSnapshot() {
761
+ if (touched) return;
683
762
  try {
684
763
  const { domToBlob } = await import("modern-screenshot");
685
764
  const width = window.innerWidth;
@@ -691,7 +770,7 @@ async function takeSnapshot() {
691
770
  type: "image/png",
692
771
  backgroundColor: getComputedStyle(document.body).backgroundColor
693
772
  });
694
- if (!blob) return;
773
+ if (!blob || touched) return;
695
774
  const [canvas, id] = [config.canvas, config.id].map(encodeURIComponent);
696
775
  await fetch(`/api/snapshots/${canvas}/${id}?theme=${theme}&height=${config.autoHeight ? height : ""}`, {
697
776
  method: "POST",
@@ -748,12 +827,31 @@ function keepLinksInFrame() {
748
827
  const original = history[name].bind(history);
749
828
  history[name] = (data, unused, url) => {
750
829
  original(data, unused, url);
751
- if (url === void 0 || url === null) return;
830
+ if (url === void 0 || url === null || !signalled) return;
752
831
  const raw = String(url);
753
832
  report(new URL(raw, location.href), false, raw, true);
754
833
  };
755
834
  }
756
835
  }
836
+ function signalReady() {
837
+ if (signalled || !loaded || holds > 0) return;
838
+ signalled = true;
839
+ window.__DESIGN_READY__ = true;
840
+ post({ type: "ready" });
841
+ scheduleSnapshot();
842
+ }
843
+ function holdReady() {
844
+ holds++;
845
+ window.__DESIGN_HOLDS__ = holds;
846
+ let released = false;
847
+ return () => {
848
+ if (released) return;
849
+ released = true;
850
+ holds--;
851
+ window.__DESIGN_HOLDS__ = holds;
852
+ requestAnimationFrame(() => requestAnimationFrame(signalReady));
853
+ };
854
+ }
757
855
  var booted = false;
758
856
  var hmrBound = false;
759
857
  var canvasHost = false;
@@ -772,6 +870,7 @@ function boot() {
772
870
  } else if (data.type === "inspect") {
773
871
  inspector ??= new FrameInspector(post);
774
872
  if (data.tokens) inspector.setTokens(data.tokens);
873
+ inspecting = !!data.on;
775
874
  if (data.on) inspector.enable();
776
875
  else inspector.disable();
777
876
  } else if (data.type === "inspect-select") inspector?.select(data.ref);
@@ -799,6 +898,19 @@ function boot() {
799
898
  if (event.key === "Escape" && !event.defaultPrevented) post({ type: "keydown", code: "Escape" });
800
899
  });
801
900
  const isModifier = (name) => name === "Meta" || name === "Control";
901
+ const touch = (event) => {
902
+ if (!event.isTrusted || inspecting) return;
903
+ touched = true;
904
+ clearTimeout(snapshotTimer);
905
+ };
906
+ window.addEventListener("pointerdown", touch, true);
907
+ window.addEventListener(
908
+ "keydown",
909
+ (event) => {
910
+ if (!isModifier(event.key) && event.key !== "Shift" && event.key !== "Alt") touch(event);
911
+ },
912
+ true
913
+ );
802
914
  window.addEventListener(
803
915
  "keydown",
804
916
  (event) => {
@@ -821,9 +933,8 @@ function boot() {
821
933
  recordError(String(reason?.message ?? reason ?? "Unhandled rejection"));
822
934
  });
823
935
  const ready = () => {
824
- window.__DESIGN_READY__ = true;
825
- post({ type: "ready" });
826
- scheduleSnapshot();
936
+ loaded = true;
937
+ signalReady();
827
938
  };
828
939
  const whenBodyReady = () => {
829
940
  if (embedded) watchSize();
@@ -840,6 +951,9 @@ function hmr(hot) {
840
951
  post({ type: "updated" });
841
952
  scheduleSnapshot();
842
953
  });
954
+ hot.on("vite:beforeFullReload", () => {
955
+ History.prototype.replaceState.call(history, history.state, "", frameUrl);
956
+ });
843
957
  hot.on("vite:error", (payload) => {
844
958
  const err = payload.err;
845
959
  recordError(err?.message ?? "Build error");
@@ -893,6 +1007,7 @@ export {
893
1007
  boot,
894
1008
  go,
895
1009
  hmr,
1010
+ holdReady,
896
1011
  mount,
897
1012
  useScreen,
898
1013
  useTheme
@@ -370,4 +370,11 @@ export type ViewerMessage = {
370
370
  type: 'inspect-select';
371
371
  ref: number | null;
372
372
  };
373
+ /** design.json `public` as a frame sees it. */
374
+ export interface PublicFolder {
375
+ /** Where the folder's files are, relative to the frame's URL. */
376
+ base: string;
377
+ /** Its files, relative to the folder, with forward slashes. */
378
+ files: string[];
379
+ }
373
380
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foss-design",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Design canvases and design systems for coding agents: live, animated, clickable screens on an infinite canvas, served locally from a .design folder.",
5
5
  "type": "module",
6
6
  "license": "MIT",