@hyperframes/studio-server 0.7.15 → 0.7.16

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
@@ -540,7 +540,7 @@ function isHTMLElement(el) {
540
540
  const HTMLEl = el.ownerDocument.defaultView?.HTMLElement;
541
541
  return HTMLEl ? el instanceof HTMLEl : "style" in el;
542
542
  }
543
- function patchStyleAttrString(style, property, value) {
543
+ function parseStyleDecls(style) {
544
544
  const props = /* @__PURE__ */ new Map();
545
545
  const order = [];
546
546
  let i = 0;
@@ -571,6 +571,13 @@ function patchStyleAttrString(style, property, value) {
571
571
  if (!props.has(key)) order.push(key);
572
572
  props.set(key, val);
573
573
  }
574
+ return { props, order };
575
+ }
576
+ function serializeStyleDecls(props, order) {
577
+ return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
578
+ }
579
+ function patchStyleAttrString(style, property, value) {
580
+ const { props, order } = parseStyleDecls(style);
574
581
  if (value === null) {
575
582
  props.delete(property);
576
583
  const idx = order.indexOf(property);
@@ -579,7 +586,7 @@ function patchStyleAttrString(style, property, value) {
579
586
  if (!props.has(property)) order.push(property);
580
587
  props.set(property, value);
581
588
  }
582
- return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
589
+ return serializeStyleDecls(props, order);
583
590
  }
584
591
  function patchElementInHtml(source, target, operations) {
585
592
  const { document: document2, wrappedFragment } = parseSourceDocument(source);
@@ -706,6 +713,127 @@ function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
706
713
  newId
707
714
  };
708
715
  }
716
+ function getInlineStylePx(el, property) {
717
+ const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
718
+ const { props } = parseStyleDecls(style);
719
+ const raw = props.get(property);
720
+ if (!raw) return 0;
721
+ const n = parseFloat(raw);
722
+ return Number.isFinite(n) ? n : 0;
723
+ }
724
+ function setInlineLeftTop(el, left, top) {
725
+ let style = el.getAttribute("style") ?? "";
726
+ style = patchStyleAttrString(style, "left", `${left}px`);
727
+ style = patchStyleAttrString(style, "top", `${top}px`);
728
+ el.setAttribute("style", style);
729
+ }
730
+ function uniqueGroupDomId(document2, groupId) {
731
+ const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
732
+ let id = base;
733
+ let n = 2;
734
+ while (document2.getElementById(id)) {
735
+ id = `${base}-${n}`;
736
+ n += 1;
737
+ }
738
+ return id;
739
+ }
740
+ function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
741
+ const { document: document2, wrappedFragment } = parseSourceDocument(source);
742
+ if (targets.length === 0) {
743
+ return { html: source, matched: false, groupId: null, error: "no targets" };
744
+ }
745
+ const els = [];
746
+ const seen = /* @__PURE__ */ new Set();
747
+ for (const target of targets) {
748
+ const el = findTargetElement(document2, target);
749
+ if (!el || !isHTMLElement(el) || seen.has(el)) continue;
750
+ seen.add(el);
751
+ els.push(el);
752
+ }
753
+ if (els.length === 0) {
754
+ return { html: source, matched: false, groupId: null, error: "no targets matched" };
755
+ }
756
+ const parent = els[0]?.parentElement;
757
+ if (!parent || els.some((el) => el.parentElement !== parent)) {
758
+ return {
759
+ html: source,
760
+ matched: false,
761
+ groupId: null,
762
+ error: "grouped elements must share a single parent"
763
+ };
764
+ }
765
+ const memberSet = new Set(els);
766
+ const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
767
+ const rebaseByEl = /* @__PURE__ */ new Map();
768
+ for (const rebase of rebases) {
769
+ const el = findTargetElement(document2, rebase.target);
770
+ if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
771
+ }
772
+ const wrapper = document2.createElement("div");
773
+ wrapper.setAttribute("data-hf-group", groupId);
774
+ wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
775
+ const memberZIndexes = ordered.map(
776
+ (el) => Number.parseInt(
777
+ parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
778
+ 10
779
+ )
780
+ ).filter((z) => Number.isFinite(z));
781
+ const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
782
+ wrapper.setAttribute(
783
+ "style",
784
+ `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
785
+ );
786
+ parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
787
+ for (const el of ordered) {
788
+ const rebase = rebaseByEl.get(el);
789
+ if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
790
+ wrapper.appendChild(el);
791
+ }
792
+ return {
793
+ html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
794
+ matched: true,
795
+ groupId
796
+ };
797
+ }
798
+ function unwrapElementsFromHtml(source, groupTarget) {
799
+ const { document: document2, wrappedFragment } = parseSourceDocument(source);
800
+ const group = findTargetElement(document2, groupTarget);
801
+ if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
802
+ if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
803
+ const parent = group.parentElement;
804
+ if (!parent) return { html: source, unwrapped: false };
805
+ const wLeft = getInlineStylePx(group, "left");
806
+ const wTop = getInlineStylePx(group, "top");
807
+ const groupCenter = {
808
+ cx: wLeft + getInlineStylePx(group, "width") / 2,
809
+ cy: wTop + getInlineStylePx(group, "height") / 2
810
+ };
811
+ const members = [];
812
+ for (const child of Array.from(group.children)) {
813
+ if (isHTMLElement(child)) {
814
+ const newLeft = getInlineStylePx(child, "left") + wLeft;
815
+ const newTop = getInlineStylePx(child, "top") + wTop;
816
+ setInlineLeftTop(child, newLeft, newTop);
817
+ if (child.id) {
818
+ members.push({
819
+ id: child.id,
820
+ cx: newLeft + getInlineStylePx(child, "width") / 2,
821
+ cy: newTop + getInlineStylePx(child, "height") / 2
822
+ });
823
+ }
824
+ }
825
+ parent.insertBefore(child, group);
826
+ }
827
+ const groupId = group.id || void 0;
828
+ group.remove();
829
+ return {
830
+ html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
831
+ unwrapped: true,
832
+ unwrappedGroupId: groupId,
833
+ members,
834
+ groupCenter
835
+ };
836
+ }
709
837
 
710
838
  // src/routes/files.ts
711
839
  import { parseHTML as parseHTML2 } from "linkedom";
@@ -844,6 +972,97 @@ function extractGsapScriptBlock(html) {
844
972
  }
845
973
  return null;
846
974
  }
975
+ function stripGsapAnimationsForSelector(html, selector) {
976
+ const block = extractGsapScriptBlock(html);
977
+ if (!block) return html;
978
+ const parsed = parseGsapScriptAcorn(block.scriptText);
979
+ const matching = parsed.animations.filter((a) => a.targetSelector === selector);
980
+ if (matching.length === 0) return html;
981
+ let script = block.scriptText;
982
+ for (const anim of [...matching].reverse()) {
983
+ script = removeAnimationFromScript(script, anim.id);
984
+ }
985
+ return block.replaceScript(script);
986
+ }
987
+ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
988
+ const block = extractGsapScriptBlock(html);
989
+ if (!block) return html;
990
+ const parsed = parseGsapScriptAcorn(block.scriptText);
991
+ const groupSel = `#${groupId}`;
992
+ const groupSets = parsed.animations.filter(
993
+ (a) => a.targetSelector === groupSel && a.method === "set"
994
+ );
995
+ if (groupSets.length === 0) return html;
996
+ const gt = {};
997
+ for (const s of groupSets) {
998
+ for (const [k, v] of Object.entries(s.properties)) if (typeof v === "number") gt[k] = v;
999
+ }
1000
+ const gx = gt.x ?? 0;
1001
+ const gy = gt.y ?? 0;
1002
+ const gz = gt.z ?? 0;
1003
+ const grot = gt.rotation ?? 0;
1004
+ const gscale = gt.scale ?? 1;
1005
+ const isScaleAxis = (k) => k === "scale" || k === "scaleX" || k === "scaleY";
1006
+ const groupIsIdentity = Object.entries(gt).every(
1007
+ ([k, v]) => isScaleAxis(k) ? v === 1 : v === 0
1008
+ );
1009
+ if (groupIsIdentity) return html;
1010
+ const rad = grot * Math.PI / 180;
1011
+ const cos = Math.cos(rad);
1012
+ const sin = Math.sin(rad);
1013
+ const round3 = (n) => Math.round(n * 1e3) / 1e3;
1014
+ let script = block.scriptText;
1015
+ for (const m of members) {
1016
+ const memberSel = `#${m.id}`;
1017
+ const sets = parsed.animations.filter(
1018
+ (a) => a.targetSelector === memberSel && a.method === "set"
1019
+ );
1020
+ const mProps = {};
1021
+ for (const s of sets) Object.assign(mProps, s.properties);
1022
+ const mx = typeof mProps.x === "number" ? mProps.x : 0;
1023
+ const my = typeof mProps.y === "number" ? mProps.y : 0;
1024
+ const dx = m.cx + mx - groupCenter.cx;
1025
+ const dy = m.cy + my - groupCenter.cy;
1026
+ const visX = groupCenter.cx + gscale * (cos * dx - sin * dy) + gx;
1027
+ const visY = groupCenter.cy + gscale * (sin * dx + cos * dy) + gy;
1028
+ const newProps = {
1029
+ ...mProps,
1030
+ x: round3(visX - m.cx),
1031
+ y: round3(visY - m.cy)
1032
+ };
1033
+ if (gz !== 0) newProps.z = (typeof mProps.z === "number" ? mProps.z : 0) + gz;
1034
+ if (grot !== 0) {
1035
+ newProps.rotation = round3(
1036
+ (typeof mProps.rotation === "number" ? mProps.rotation : 0) + grot
1037
+ );
1038
+ }
1039
+ if (gscale !== 1) {
1040
+ newProps.scale = round3((typeof mProps.scale === "number" ? mProps.scale : 1) * gscale);
1041
+ }
1042
+ const pivoted = /* @__PURE__ */ new Set(["x", "y", "z", "rotation", "scale"]);
1043
+ for (const [k, v] of Object.entries(gt)) {
1044
+ if (pivoted.has(k) || typeof v !== "number") continue;
1045
+ if (k === "scaleX" || k === "scaleY") {
1046
+ if (v !== 1) newProps[k] = round3((typeof mProps[k] === "number" ? mProps[k] : 1) * v);
1047
+ } else if (k === "transformPerspective") {
1048
+ if (typeof mProps[k] !== "number") newProps[k] = v;
1049
+ } else if (v !== 0) {
1050
+ newProps[k] = round3((typeof mProps[k] === "number" ? mProps[k] : 0) + v);
1051
+ }
1052
+ }
1053
+ for (const s of [...sets].reverse()) {
1054
+ script = removeAnimationFromScript(script, s.id);
1055
+ }
1056
+ script = addAnimationToScript(script, {
1057
+ targetSelector: memberSel,
1058
+ method: "set",
1059
+ position: 0,
1060
+ properties: newProps,
1061
+ global: true
1062
+ }).script;
1063
+ }
1064
+ return block.replaceScript(script);
1065
+ }
847
1066
  function stripStudioEditsFromTarget(document2, selector) {
848
1067
  if (!selector) return 0;
849
1068
  let stripped = 0;
@@ -1692,6 +1911,88 @@ function registerFileRoutes(api, adapter) {
1692
1911
  backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
1693
1912
  });
1694
1913
  });
1914
+ api.post("/projects/:id/file-mutations/wrap-elements/*", async (c) => {
1915
+ const ctx = await resolveFileMutationContext(c, adapter, "wrap-elements");
1916
+ if ("error" in ctx) return ctx.error;
1917
+ const body = await c.req.json().catch(() => null);
1918
+ if (!Array.isArray(body?.targets) || body.targets.length === 0 || !body.groupId) {
1919
+ return c.json({ error: "targets and groupId required" }, 400);
1920
+ }
1921
+ const bbox = body.bbox ?? {};
1922
+ const bboxNums = [bbox.left, bbox.top, bbox.width, bbox.height];
1923
+ const rebases = body.rebases ?? [];
1924
+ const allNumeric = bboxNums.every((n) => typeof n === "number" && Number.isFinite(n)) && rebases.every(
1925
+ (r) => typeof r?.left === "number" && Number.isFinite(r.left) && typeof r?.top === "number" && Number.isFinite(r.top)
1926
+ );
1927
+ if (!allNumeric) {
1928
+ return c.json({ error: "bbox and rebase coordinates must be finite numbers" }, 400);
1929
+ }
1930
+ let originalContent;
1931
+ try {
1932
+ originalContent = readFileSync3(ctx.absPath, "utf-8");
1933
+ } catch {
1934
+ return c.json({ error: "not found" }, 404);
1935
+ }
1936
+ const result = wrapElementsInHtml(
1937
+ originalContent,
1938
+ body.targets,
1939
+ body.groupId,
1940
+ { left: bbox.left, top: bbox.top, width: bbox.width, height: bbox.height },
1941
+ rebases
1942
+ );
1943
+ if (!result.matched) {
1944
+ return c.json(
1945
+ {
1946
+ ok: false,
1947
+ changed: false,
1948
+ content: originalContent,
1949
+ path: ctx.filePath,
1950
+ error: result.error
1951
+ },
1952
+ result.error === "grouped elements must share a single parent" ? 422 : 400
1953
+ );
1954
+ }
1955
+ const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
1956
+ if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
1957
+ writeFileSync4(ctx.absPath, result.html, "utf-8");
1958
+ return c.json({
1959
+ ok: true,
1960
+ changed: true,
1961
+ groupId: result.groupId,
1962
+ content: result.html,
1963
+ path: ctx.filePath,
1964
+ backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
1965
+ });
1966
+ });
1967
+ api.post("/projects/:id/file-mutations/unwrap-elements/*", async (c) => {
1968
+ const ctx = await resolveFileMutationContext(c, adapter, "unwrap-elements");
1969
+ if ("error" in ctx) return ctx.error;
1970
+ const parsed = await parseMutationBody(c);
1971
+ if ("error" in parsed) return parsed.error;
1972
+ let originalContent;
1973
+ try {
1974
+ originalContent = readFileSync3(ctx.absPath, "utf-8");
1975
+ } catch {
1976
+ return c.json({ error: "not found" }, 404);
1977
+ }
1978
+ const result = unwrapElementsFromHtml(originalContent, parsed.target);
1979
+ if (!result.unwrapped) {
1980
+ return c.json({ ok: false, changed: false, content: originalContent, path: ctx.filePath });
1981
+ }
1982
+ let cleaned = result.html;
1983
+ if (result.unwrappedGroupId && result.members && result.groupCenter) {
1984
+ cleaned = bakeGroupTransformIntoMembers(
1985
+ cleaned,
1986
+ result.unwrappedGroupId,
1987
+ result.members,
1988
+ result.groupCenter
1989
+ );
1990
+ }
1991
+ if (result.unwrappedGroupId) {
1992
+ cleaned = stripGsapAnimationsForSelector(cleaned, `#${result.unwrappedGroupId}`);
1993
+ }
1994
+ return writeIfChanged(c, ctx.project.dir, ctx.filePath, ctx.absPath, originalContent, cleaned);
1995
+ });
1695
1996
  api.post("/projects/:id/file-mutations/probe-element/*", async (c) => {
1696
1997
  const ctx = await resolveFileMutationContext(c, adapter, "probe-element");
1697
1998
  if ("error" in ctx) return ctx.error;