@hyperframes/studio 0.8.142 → 0.8.144

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.
Files changed (76) hide show
  1. package/dist/assets/hyperframes-player-Bw_jIaSX.js +473 -0
  2. package/dist/assets/{index-B-wffzRH.js → index-BVM9NuWl.js} +153 -153
  3. package/dist/assets/{index-D3Vij9t1.js → index-Cj7W_c7p.js} +1 -1
  4. package/dist/assets/{index-D5q2_7Gw.js → index-DyH9dT7b.js} +1 -1
  5. package/dist/{chunk-TFLU2MGI.js → chunk-3GHTHPZD.js} +8 -1
  6. package/dist/chunk-3GHTHPZD.js.map +1 -0
  7. package/dist/{chunk-H2DUOXTF.js → chunk-G3EDKRZH.js} +3 -3
  8. package/dist/{chunk-UYWON5AX.js → chunk-PEAKCOUW.js} +7 -3
  9. package/dist/chunk-PEAKCOUW.js.map +1 -0
  10. package/dist/{chunk-LOVBLSMR.js → chunk-Y2OHHNM2.js} +35 -19
  11. package/dist/chunk-Y2OHHNM2.js.map +1 -0
  12. package/dist/{domEditingLayers-KBTPQKGA.js → domEditingLayers-PYST4G45.js} +3 -3
  13. package/dist/index.html +1 -1
  14. package/dist/index.js +261 -230
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +3 -3
  17. package/dist/ui.d.ts +4 -5
  18. package/dist/ui.js +1 -1
  19. package/package.json +8 -8
  20. package/src/components/MediaPreview.tsx +2 -1
  21. package/src/components/editor/DomEditSelectionChrome.test.tsx +103 -0
  22. package/src/components/editor/DomEditSelectionChrome.tsx +42 -16
  23. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +36 -0
  24. package/src/components/editor/domEditOverlayGestures.ts +1 -0
  25. package/src/components/editor/domEditOverlayStartGesture.ts +5 -0
  26. package/src/components/editor/domEditResizeLocal.test.ts +81 -1
  27. package/src/components/editor/domEditResizeLocal.ts +27 -3
  28. package/src/components/editor/domEditingLayers.ts +4 -1
  29. package/src/components/editor/domEditingTextFields.test.ts +18 -0
  30. package/src/components/editor/manualEditsDom.ts +12 -12
  31. package/src/components/editor/mediaAudioEdits.test.ts +12 -0
  32. package/src/components/editor/mediaAudioEdits.ts +5 -5
  33. package/src/components/editor/previewGestureStarts.ts +14 -5
  34. package/src/components/editor/propertyPanelFill.tsx +2 -1
  35. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -4
  36. package/src/components/editor/propertyPanelMediaSection.tsx +3 -4
  37. package/src/components/editor/resizeDraft.ts +13 -4
  38. package/src/components/editor/useColorGradingController.ts +2 -1
  39. package/src/components/editor/useDomEditOverlayGestures.ts +5 -3
  40. package/src/components/ui/Menu.test.tsx +67 -5
  41. package/src/components/ui/Menu.tsx +21 -4
  42. package/src/components/ui/Popover.tsx +11 -5
  43. package/src/hooks/gsapResizeIntercept.test.ts +46 -11
  44. package/src/hooks/gsapResizeIntercept.ts +25 -20
  45. package/src/hooks/useDomEditCommits.ts +2 -1
  46. package/src/hooks/useFileManager.ts +2 -1
  47. package/src/hooks/useFileTree.ts +2 -1
  48. package/src/hooks/useRenderClipContent.ts +15 -3
  49. package/src/hooks/useThumbnailStripSize.test.tsx +123 -0
  50. package/src/hooks/useThumbnailStripSize.ts +19 -8
  51. package/src/player/components/CompositionThumbnail.test.ts +3 -0
  52. package/src/player/components/ImageThumbnail.test.tsx +4 -1
  53. package/src/player/components/VideoThumbnail.test.tsx +13 -2
  54. package/src/player/components/clipMenuNormalize.ts +2 -2
  55. package/src/player/components/clipPeakMap.ts +2 -2
  56. package/src/player/components/thumbnailUtils.test.ts +33 -0
  57. package/src/player/components/thumbnailUtils.ts +5 -0
  58. package/src/player/hooks/timelineSyncHydration.ts +2 -1
  59. package/src/player/lib/mediaProbe.test.ts +12 -0
  60. package/src/player/lib/mediaProbe.ts +4 -2
  61. package/src/utils/authoredSource.test.ts +25 -0
  62. package/src/utils/authoredSource.ts +3 -5
  63. package/src/utils/rounding.ts +7 -0
  64. package/src/utils/sourcePatcher.test.ts +135 -0
  65. package/src/utils/sourcePatcher.ts +102 -186
  66. package/src/utils/studioHelpers.test.ts +23 -0
  67. package/src/utils/studioHelpers.ts +3 -2
  68. package/src/utils/studioSelectionSnapshot.test.ts +31 -0
  69. package/src/utils/timelineAssetDrop.test.ts +14 -0
  70. package/src/utils/timelineAssetDrop.ts +2 -1
  71. package/dist/assets/hyperframes-player-sAsRuV73.js +0 -473
  72. package/dist/chunk-LOVBLSMR.js.map +0 -1
  73. package/dist/chunk-TFLU2MGI.js.map +0 -1
  74. package/dist/chunk-UYWON5AX.js.map +0 -1
  75. /package/dist/{chunk-H2DUOXTF.js.map → chunk-G3EDKRZH.js.map} +0 -0
  76. /package/dist/{domEditingLayers-KBTPQKGA.js.map → domEditingLayers-PYST4G45.js.map} +0 -0
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  usePreviewVariablesStore,
41
41
  whenPreviewBooted,
42
42
  writeStudioUiPreferences
43
- } from "./chunk-H2DUOXTF.js";
43
+ } from "./chunk-G3EDKRZH.js";
44
44
  import {
45
45
  Button,
46
46
  ContextMenu,
@@ -72,7 +72,7 @@ import {
72
72
  fieldBase,
73
73
  fieldText,
74
74
  popupSurface
75
- } from "./chunk-LOVBLSMR.js";
75
+ } from "./chunk-Y2OHHNM2.js";
76
76
  import {
77
77
  DOM_EDIT_LAYER_OBSERVER_INIT,
78
78
  STUDIO_FLAT_INSPECTOR_ENABLED,
@@ -105,7 +105,7 @@ import {
105
105
  resolveDomEditSelection,
106
106
  serializeDomEditTextFields,
107
107
  studioApiFetch
108
- } from "./chunk-UYWON5AX.js";
108
+ } from "./chunk-PEAKCOUW.js";
109
109
  import {
110
110
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
111
111
  LAYER_REVEAL_PRIOR_Z_ATTR,
@@ -158,13 +158,14 @@ import {
158
158
  roundTo3,
159
159
  roundToCenti,
160
160
  roundToLayoutPx,
161
+ roundToLayoutSize,
161
162
  runtimeAudioId,
162
163
  sameTimelineText,
163
164
  setCompositionSourceMap,
164
165
  splitTimelineElementKey,
165
166
  toHexColor,
166
167
  wrapTimeline
167
- } from "./chunk-TFLU2MGI.js";
168
+ } from "./chunk-3GHTHPZD.js";
168
169
  import "./chunk-PZ5AY32C.js";
169
170
 
170
171
  // src/components/EditorShell.tsx
@@ -3597,6 +3598,7 @@ function useTimelineZoom() {
3597
3598
  import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState4 } from "react";
3598
3599
 
3599
3600
  // src/utils/timelineAssetDrop.ts
3601
+ import { encodeUrlPath } from "@hyperframes/parsers";
3600
3602
  import { buildTimelineAssetId } from "@hyperframes/core/timeline-asset-id";
3601
3603
 
3602
3604
  // src/utils/mediaTypes.ts
@@ -3701,7 +3703,7 @@ function fitTimelineAssetGeometry(natural, comp) {
3701
3703
  };
3702
3704
  }
3703
3705
  function buildTimelineAssetInsertHtml(input) {
3704
- const sharedAttrs = `id="${input.id}" data-hf-id="${input.hfId}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
3706
+ const sharedAttrs = `id="${input.id}" data-hf-id="${input.hfId}" class="clip" src="${encodeUrlPath(input.assetPath)}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
3705
3707
  const geometry = input.geometry ?? { left: 0, top: 0, width: 640, height: 360 };
3706
3708
  const visualStyles = `position: absolute; left: ${geometry.left}px; top: ${geometry.top}px; width: ${geometry.width}px; height: ${geometry.height}px; object-fit: contain; z-index: ${input.zIndex}`;
3707
3709
  if (input.kind === "image") {
@@ -16485,17 +16487,24 @@ var measure = (entries) => isTimelineZoomPreviewing() || commit(
16485
16487
  })
16486
16488
  );
16487
16489
  var onPresence = (entries) => measure(entries.filter((entry2) => entry2.isIntersecting));
16488
- var onResize = (entries) => measure(
16489
- entries.map(({ target, contentRect: { width, height } }) => ({
16490
- target,
16491
- size: { width, height }
16492
- }))
16493
- );
16490
+ var pendingSizes = /* @__PURE__ */ new Map();
16491
+ var pendingSizesFrame = 0;
16492
+ var applyPendingSizes = () => {
16493
+ pendingSizesFrame = 0;
16494
+ const entries = [...pendingSizes].map(([target, size]) => ({ target, size }));
16495
+ pendingSizes.clear();
16496
+ measure(entries);
16497
+ };
16498
+ var applyResizesNextFrame = (entries) => {
16499
+ for (const { target, contentRect } of entries)
16500
+ pendingSizes.set(target, { width: contentRect.width, height: contentRect.height });
16501
+ pendingSizesFrame ||= requestAnimationFrame(applyPendingSizes);
16502
+ };
16494
16503
  var observeIntersections = (callback) => typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver(callback, GAP_WARNING);
16495
16504
  function acquire() {
16496
16505
  if (users++ === 0) {
16497
16506
  shared = {
16498
- resize: new ResizeObserver(onResize),
16507
+ resize: new ResizeObserver(applyResizesNextFrame),
16499
16508
  presence: observeIntersections(onPresence),
16500
16509
  gaps: observeIntersections(
16501
16510
  (entries) => entries.some((entry2) => entry2.isIntersecting) && scheduleRefresh()
@@ -16516,6 +16525,9 @@ function release() {
16516
16525
  window.removeEventListener("scroll", scheduleRefresh, { capture: true });
16517
16526
  cancelAnimationFrame(frame2);
16518
16527
  frame2 = 0;
16528
+ cancelAnimationFrame(pendingSizesFrame);
16529
+ pendingSizesFrame = 0;
16530
+ pendingSizes.clear();
16519
16531
  }
16520
16532
  var watchGap = (gap) => {
16521
16533
  if (!gap) return;
@@ -16556,6 +16568,7 @@ function useThumbnailStripSize() {
16556
16568
  resize.unobserve(target);
16557
16569
  presence?.unobserve(target);
16558
16570
  strips.delete(target);
16571
+ pendingSizes.delete(target);
16559
16572
  release();
16560
16573
  };
16561
16574
  }, []);
@@ -17143,17 +17156,41 @@ function useOwnPreviewIframe() {
17143
17156
  import { isHtmlElement as isHtmlElement4 } from "@hyperframes/core/runtime/dom-realm";
17144
17157
 
17145
17158
  // src/utils/sourcePatcher.ts
17159
+ import {
17160
+ scanHtmlOpeningTags,
17161
+ decodeAuthoredAttribute,
17162
+ HTML_ATTRIBUTE_ENTITIES
17163
+ } from "@hyperframes/parsers";
17146
17164
  function escapeRegex(s) {
17147
17165
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17148
17166
  }
17167
+ function tagAttribute(tag, name) {
17168
+ return scanHtmlOpeningTags(tag)[0]?.attributes.find((attr) => attr.name === name.toLowerCase());
17169
+ }
17170
+ function patchTagAttribute(tag, name, edit) {
17171
+ if (edit.kind === "remove") {
17172
+ const attributes = scanHtmlOpeningTags(tag)[0]?.attributes.filter((attr2) => attr2.name === name.toLowerCase()) ?? [];
17173
+ const parts = [];
17174
+ let cursor = 0;
17175
+ for (const attribute of attributes) {
17176
+ let start = attribute.start;
17177
+ while (start > cursor && /[\t\n\f\r ]/.test(tag[start - 1])) start--;
17178
+ parts.push(tag.slice(cursor, start));
17179
+ cursor = attribute.end;
17180
+ }
17181
+ parts.push(tag.slice(cursor));
17182
+ return parts.join("");
17183
+ }
17184
+ const attr = tagAttribute(tag, name);
17185
+ const replacement = edit.kind === "boolean" ? name : `${name}="${escapeHtmlAttribute(edit.value)}"`;
17186
+ if (attr) return tag.slice(0, attr.start) + replacement + tag.slice(attr.end);
17187
+ return tag.endsWith("/") ? `${tag.slice(0, -1).trimEnd()} ${replacement} /` : `${tag} ${replacement}`;
17188
+ }
17149
17189
  function escapeStyleAttributeValue(value, quote) {
17150
17190
  return quote === '"' ? value.replace(/"/g, """) : value.replace(/'/g, "'");
17151
17191
  }
17152
17192
  function escapeHtmlAttribute(value) {
17153
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
17154
- }
17155
- function unescapeHtmlAttribute(value) {
17156
- return value.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
17193
+ return value.replace(/[&"<>]/g, (char) => HTML_ATTRIBUTE_ENTITIES[char][0]);
17157
17194
  }
17158
17195
  function splitInlineStyleDeclarations(style) {
17159
17196
  const declarations = [];
@@ -17206,7 +17243,7 @@ function resolveSourceFile(elementId2, selector, files) {
17206
17243
  if (!elementId2 && !selector) return null;
17207
17244
  if (elementId2) {
17208
17245
  for (const [path, content] of Object.entries(files)) {
17209
- if (content.includes(`id="${elementId2}"`) || content.includes(`id='${elementId2}'`)) {
17246
+ if (findTagByAttribute(content, "id", elementId2)) {
17210
17247
  return path;
17211
17248
  }
17212
17249
  }
@@ -17215,7 +17252,7 @@ function resolveSourceFile(elementId2, selector, files) {
17215
17252
  if (compIdMatch) {
17216
17253
  const compId = compIdMatch[1];
17217
17254
  for (const [path, content] of Object.entries(files)) {
17218
- if (content.includes(`data-composition-id="${compId}"`)) {
17255
+ if (findTagByAttribute(content, "data-composition-id", compId)) {
17219
17256
  return path;
17220
17257
  }
17221
17258
  }
@@ -17224,7 +17261,7 @@ function resolveSourceFile(elementId2, selector, files) {
17224
17261
  if (classMatch) {
17225
17262
  const cls = classMatch[1];
17226
17263
  for (const [path, content] of Object.entries(files)) {
17227
- if (content.includes(`class="${cls}"`) || content.includes(`class="${cls} `) || content.includes(` ${cls}"`)) {
17264
+ if (findTagByClass(content, { selector: `.${cls}` })) {
17228
17265
  return path;
17229
17266
  }
17230
17267
  }
@@ -17233,18 +17270,14 @@ function resolveSourceFile(elementId2, selector, files) {
17233
17270
  return null;
17234
17271
  }
17235
17272
  function patchInlineStyle(html2, elementId2, prop, value) {
17236
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17237
- const match = idPattern.exec(html2);
17238
- if (!match) return html2;
17239
- const tag = match[1];
17240
- return patchInlineStyleInTag(html2, tag, prop, value);
17241
- }
17242
- function patchInlineStyleInTag(html2, tag, prop, value) {
17243
- if (!tag) return html2;
17244
- const styleMatch = /\bstyle=(")([^"]*)"|\bstyle=(')([^']*)'/.exec(tag);
17245
- if (styleMatch) {
17246
- const existingStyle = styleMatch[2] ?? styleMatch[4];
17247
- const quote = styleMatch[1] ?? styleMatch[3];
17273
+ return patchInlineStyleByTarget(html2, { id: elementId2 }, prop, value);
17274
+ }
17275
+ function patchInlineStyleInTag(tag, prop, value) {
17276
+ if (!tag) return tag;
17277
+ const styleMatch = tagAttribute(tag, "style");
17278
+ if (styleMatch?.kind === "value") {
17279
+ const existingStyle = styleMatch.value;
17280
+ const quote = styleMatch.quote || '"';
17248
17281
  const props = /* @__PURE__ */ new Map();
17249
17282
  for (const part of splitInlineStyleDeclarations(existingStyle)) {
17250
17283
  const colon = part.indexOf(":");
@@ -17259,67 +17292,54 @@ function patchInlineStyleInTag(html2, tag, prop, value) {
17259
17292
  props.set(prop, value);
17260
17293
  }
17261
17294
  const newStyle = Array.from(props.entries()).map(([k, v]) => `${k}: ${escapeStyleAttributeValue(v, quote)}`).join("; ");
17262
- const newTag = tag.replace(styleMatch[0], `style=${quote}${newStyle}${quote}`);
17263
- return html2.replace(tag, newTag);
17295
+ const newTag = tag.slice(0, styleMatch.start) + `style=${quote}${newStyle}${quote}` + tag.slice(styleMatch.end);
17296
+ return newTag;
17264
17297
  } else {
17265
- if (value === null) return html2;
17298
+ if (value === null) return tag;
17266
17299
  const selfClosing = tag.endsWith("/");
17267
17300
  const base2 = selfClosing ? tag.slice(0, -1).trimEnd() : tag;
17268
17301
  const newTag = `${base2} style="${prop}: ${escapeStyleAttributeValue(value, '"')}"${selfClosing ? " /" : ""}`;
17269
- return html2.replace(tag, newTag);
17302
+ return newTag;
17270
17303
  }
17271
17304
  }
17272
17305
  function patchInlineStyleByTarget(html2, target, prop, value) {
17273
17306
  const match = findTagByTarget(html2, target);
17274
17307
  if (!match) return html2;
17275
- const newTag = patchInlineStyleInTag(match.tag, match.tag, prop, value);
17308
+ const newTag = patchInlineStyleInTag(match.tag, prop, value);
17276
17309
  return replaceTagAtMatch(html2, match, newTag);
17277
17310
  }
17278
17311
  function replaceTagAtMatch(html2, match, newTag) {
17279
17312
  return `${html2.slice(0, match.start)}${newTag}${html2.slice(match.end)}`;
17280
17313
  }
17281
- function execDataAttrPattern(html2, attr, value) {
17282
- const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i");
17283
- const match = pattern.exec(html2);
17284
- if (match?.index == null) return null;
17285
- return { tag: match[1], start: match.index, end: match.index + match[1].length };
17314
+ function findTagByAttribute(html2, name, value) {
17315
+ const found = scanHtmlOpeningTags(html2).find((tag) => {
17316
+ const attr = tag.attributes.find((attr2) => attr2.name === name);
17317
+ return tag.closed && attr?.kind === "value" && decodeAuthoredAttribute(attr.value) === value;
17318
+ });
17319
+ return found ? { tag: html2.slice(found.start, found.bodyEnd), start: found.start, end: found.bodyEnd } : null;
17286
17320
  }
17287
17321
  function findTagByClass(html2, target) {
17288
17322
  const classMatch = target.selector?.match(/^\.([a-zA-Z0-9_-]+)$/);
17289
17323
  if (!classMatch) return null;
17290
- const cls = classMatch[1];
17291
- const pattern = new RegExp(
17292
- `(<[^>]*\\bclass=(["'])[^"']*\\b${escapeRegex(cls)}\\b[^"']*\\2[^>]*)>`,
17293
- "gi"
17294
- );
17295
- const selectorIndex = target.selectorIndex ?? 0;
17296
- let match;
17297
- let currentIndex = 0;
17298
- while ((match = pattern.exec(html2)) !== null) {
17299
- if (currentIndex === selectorIndex && match.index != null) {
17300
- return {
17301
- tag: match[1],
17302
- start: match.index,
17303
- end: match.index + match[1].length
17304
- };
17305
- }
17306
- currentIndex += 1;
17307
- }
17308
- return null;
17324
+ const found = scanHtmlOpeningTags(html2).filter((tag) => {
17325
+ const attr = tag.attributes.find((attr2) => attr2.name === "class");
17326
+ return tag.closed && attr?.kind === "value" && decodeAuthoredAttribute(attr.value).split(/\s+/).includes(classMatch[1]);
17327
+ })[target.selectorIndex ?? 0];
17328
+ return found ? { tag: html2.slice(found.start, found.bodyEnd), start: found.start, end: found.bodyEnd } : null;
17309
17329
  }
17310
17330
  function findTagByTarget(html2, target) {
17311
17331
  if (target.hfId) {
17312
- const result = execDataAttrPattern(html2, "data-hf-id", target.hfId);
17332
+ const result = findTagByAttribute(html2, "data-hf-id", target.hfId);
17313
17333
  if (result) return result;
17314
17334
  }
17315
17335
  if (target.id) {
17316
- const result = execDataAttrPattern(html2, "id", target.id);
17336
+ const result = findTagByAttribute(html2, "id", target.id);
17317
17337
  if (result) return result;
17318
17338
  }
17319
17339
  if (!target.selector) return null;
17320
17340
  const compositionIdMatch = target.selector.match(/^\[data-composition-id="([^"]+)"\]$/);
17321
17341
  if (compositionIdMatch) {
17322
- const result = execDataAttrPattern(html2, "data-composition-id", compositionIdMatch[1]);
17342
+ const result = findTagByAttribute(html2, "data-composition-id", compositionIdMatch[1]);
17323
17343
  if (result) return result;
17324
17344
  }
17325
17345
  return findTagByClass(html2, target);
@@ -17327,9 +17347,11 @@ function findTagByTarget(html2, target) {
17327
17347
  function readAttributeByTarget(html2, target, attr) {
17328
17348
  const match = findTagByTarget(html2, target);
17329
17349
  if (!match) return void 0;
17330
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17331
- const valueMatch = new RegExp(`\\b${fullAttr}=(["'])([^"']*)\\1`).exec(match.tag);
17332
- return valueMatch?.[2] != null ? unescapeHtmlAttribute(valueMatch[2]) : void 0;
17350
+ return readTagAttribute(match.tag, attr.startsWith("data-") ? attr : `data-${attr}`);
17351
+ }
17352
+ function readTagAttribute(tag, attr) {
17353
+ const value = tagAttribute(tag, attr);
17354
+ return value?.kind === "value" ? decodeAuthoredAttribute(value.value) : void 0;
17333
17355
  }
17334
17356
  function readTagSnippetByTarget(html2, target) {
17335
17357
  const match = findTagByTarget(html2, target);
@@ -17338,60 +17360,15 @@ function readTagSnippetByTarget(html2, target) {
17338
17360
  function patchAttributeByTarget(html2, target, attr, value) {
17339
17361
  const match = findTagByTarget(html2, target);
17340
17362
  if (!match) return html2;
17341
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17342
- const attrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}=(["'])([^"']*)\\1`);
17343
- const tag = match.tag;
17344
- if (value === null) {
17345
- const boolAttrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17346
- if (!boolAttrPattern.test(tag)) return html2;
17347
- const removePattern = new RegExp(`\\s+${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17348
- const newTag2 = tag.replace(removePattern, "");
17349
- return replaceTagAtMatch(html2, match, newTag2);
17350
- }
17351
- const escaped = escapeHtmlAttribute(value);
17352
- if (attrPattern.test(tag)) {
17353
- const newTag2 = tag.replace(attrPattern, `${fullAttr}="${escaped}"`);
17354
- return replaceTagAtMatch(html2, match, newTag2);
17355
- }
17356
- const newTag = tag + ` ${fullAttr}="${escaped}"`;
17357
- return replaceTagAtMatch(html2, match, newTag);
17363
+ const name = attr.startsWith("data-") ? attr : `data-${attr}`;
17364
+ const edit = value === null ? { kind: "remove" } : { kind: "value", value };
17365
+ return replaceTagAtMatch(html2, match, patchTagAttribute(match.tag, name, edit));
17358
17366
  }
17359
17367
  function patchAttribute(html2, elementId2, attr, value) {
17360
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17361
- const match = idPattern.exec(html2);
17362
- if (!match) return html2;
17363
- const tag = match[1];
17364
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17365
- const attrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}=(["'])([^"']*)\\1`);
17366
- if (value === null) {
17367
- const boolAttrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17368
- if (!boolAttrPattern.test(tag)) return html2;
17369
- const removePattern = new RegExp(`\\s+${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17370
- const newTag = tag.replace(removePattern, "");
17371
- return html2.replace(tag, newTag);
17372
- }
17373
- const escaped = escapeHtmlAttribute(value);
17374
- if (attrPattern.test(tag)) {
17375
- const newTag = tag.replace(attrPattern, `${fullAttr}="${escaped}"`);
17376
- return html2.replace(tag, newTag);
17377
- } else {
17378
- const newTag = tag + ` ${fullAttr}="${escaped}"`;
17379
- return html2.replace(tag, newTag);
17380
- }
17368
+ return patchAttributeByTarget(html2, { id: elementId2 }, attr, value);
17381
17369
  }
17382
17370
  function patchTextContent(html2, elementId2, value) {
17383
- const openTagPattern = new RegExp(
17384
- `(<([a-z0-9-]+)[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\3[^>]*>)`,
17385
- "i"
17386
- );
17387
- const match = openTagPattern.exec(html2);
17388
- if (!match || match.index == null) return html2;
17389
- const openingTag = match[1];
17390
- const tagName = match[2];
17391
- const contentStart = match.index + openingTag.length;
17392
- const closingIndex = findMatchingClosingTagIndex(html2, tagName, contentStart);
17393
- if (closingIndex < 0) return html2;
17394
- return `${html2.slice(0, contentStart)}${value}${html2.slice(closingIndex)}`;
17371
+ return patchTextContentByTarget(html2, { id: elementId2 }, value);
17395
17372
  }
17396
17373
  function findMatchingClosingTagIndex(html2, tagName, contentStart) {
17397
17374
  const tagPattern = new RegExp(`</?${escapeRegex(tagName)}\\b[^>]*>`, "gi");
@@ -17426,47 +17403,26 @@ var HTML_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
17426
17403
  "reversed",
17427
17404
  "selected"
17428
17405
  ]);
17429
- function patchHtmlAttributeInTag(html2, tag, attr, value) {
17430
- if (!tag) return html2;
17431
- const isBoolean = HTML_BOOLEAN_ATTRIBUTES.has(attr);
17432
- if (isBoolean) {
17433
- const escapedAttr = escapeRegex(attr);
17434
- const hasBoolAttr = new RegExp(`(?:^|\\s)${escapedAttr}(?:\\s|=|$)`).test(tag);
17435
- if (value === null || value === "" || value === "false") {
17436
- if (!hasBoolAttr) return html2;
17437
- const removePattern = new RegExp(`\\s+${escapedAttr}(?:=(["'])[^"']*\\1)?`);
17438
- const newTag3 = tag.replace(removePattern, "");
17439
- return html2.replace(tag, newTag3);
17440
- }
17441
- if (hasBoolAttr) return html2;
17442
- const newTag2 = tag + ` ${attr}`;
17443
- return html2.replace(tag, newTag2);
17444
- }
17445
- const attrPattern = new RegExp(`\\b${escapeRegex(attr)}=(["'])([^"']*)\\1`);
17446
- if (value === null) {
17447
- if (!attrPattern.test(tag)) return html2;
17448
- const removePattern = new RegExp(`\\s+${escapeRegex(attr)}=(["'])[^"']*\\1`);
17449
- const newTag2 = tag.replace(removePattern, "");
17450
- return html2.replace(tag, newTag2);
17451
- }
17452
- const escaped = escapeHtmlAttribute(value);
17453
- if (attrPattern.test(tag)) {
17454
- const newTag2 = tag.replace(attrPattern, `${attr}="${escaped}"`);
17455
- return html2.replace(tag, newTag2);
17456
- }
17457
- const newTag = tag + ` ${attr}="${escaped}"`;
17458
- return html2.replace(tag, newTag);
17406
+ function patchHtmlAttributeInTag(tag, attr, value) {
17407
+ if (!tag) return tag;
17408
+ let edit;
17409
+ if (HTML_BOOLEAN_ATTRIBUTES.has(attr)) {
17410
+ if (value === null || value === "" || value === "false") edit = { kind: "remove" };
17411
+ else {
17412
+ if (tagAttribute(tag, attr)) return tag;
17413
+ edit = { kind: "boolean" };
17414
+ }
17415
+ } else edit = value === null ? { kind: "remove" } : { kind: "value", value };
17416
+ const newTag = patchTagAttribute(tag, attr, edit);
17417
+ return newTag;
17459
17418
  }
17460
17419
  function patchHtmlAttribute(html2, elementId2, attr, value) {
17461
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17462
- const match = idPattern.exec(html2);
17463
- if (!match) return html2;
17464
- return patchHtmlAttributeInTag(html2, match[1], attr, value);
17420
+ return patchHtmlAttributeByTarget(html2, { id: elementId2 }, attr, value);
17465
17421
  }
17466
17422
  function patchHtmlAttributeByTarget(html2, target, attr, value) {
17467
17423
  const match = findTagByTarget(html2, target);
17468
17424
  if (!match) return html2;
17469
- const newTag = patchHtmlAttributeInTag(match.tag, match.tag, attr, value);
17425
+ const newTag = patchHtmlAttributeInTag(match.tag, attr, value);
17470
17426
  return replaceTagAtMatch(html2, match, newTag);
17471
17427
  }
17472
17428
  function patchTextContentByTarget(html2, target, value) {
@@ -17631,8 +17587,8 @@ import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
17631
17587
  // src/utils/authoredSource.ts
17632
17588
  import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
17633
17589
  import {
17634
- STUDIO_PREVIEW_LAZY_ATTR,
17635
- STUDIO_PREVIEW_UPCOMING_ATTR
17590
+ PREVIEW_ONLY_ATTRS,
17591
+ STUDIO_PREVIEW_LAZY_ATTR
17636
17592
  } from "@hyperframes/core/studio-preview-mark";
17637
17593
  function parseSavedSource(html2) {
17638
17594
  return new DOMParser().parseFromString(ensureHfIds(html2), "text/html");
@@ -17713,10 +17669,8 @@ function authoredMarkup(authored, live, sourceFile) {
17713
17669
  function liveMarkupWithoutPreviewMarks(live) {
17714
17670
  const copy = live.cloneNode(true);
17715
17671
  for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
17716
- el.removeAttribute(STUDIO_PREVIEW_UPCOMING_ATTR);
17717
- if (!el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) continue;
17718
- el.removeAttribute("loading");
17719
- el.removeAttribute(STUDIO_PREVIEW_LAZY_ATTR);
17672
+ if (el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) el.removeAttribute("loading");
17673
+ for (const attr of PREVIEW_ONLY_ATTRS) el.removeAttribute(attr);
17720
17674
  }
17721
17675
  return copy.outerHTML;
17722
17676
  }
@@ -21837,6 +21791,7 @@ import {
21837
21791
  } from "@hyperframes/core/audio-automation";
21838
21792
 
21839
21793
  // src/utils/studioHelpers.ts
21794
+ import { encodeUrlPath as encodeUrlPath2 } from "@hyperframes/parsers";
21840
21795
  function getTimelineElementLabel(element) {
21841
21796
  return element.label || element.id || element.tag;
21842
21797
  }
@@ -21991,7 +21946,7 @@ async function resolveDroppedAssetDuration(projectId, assetPath, kind) {
21991
21946
  if (kind === "image") return DEFAULT_TIMELINE_ASSET_DURATION.image;
21992
21947
  const media = document.createElement(kind === "video" ? "video" : "audio");
21993
21948
  media.preload = "metadata";
21994
- media.src = buildProjectApiPath(projectId, `/preview/${assetPath}`);
21949
+ media.src = buildProjectApiPath(projectId, `/preview/${encodeUrlPath2(assetPath)}`);
21995
21950
  const duration = await new Promise((resolve) => {
21996
21951
  const timeout = window.setTimeout(() => resolve(DEFAULT_TIMELINE_ASSET_DURATION[kind]), 3e3);
21997
21952
  const finalize = (value) => {
@@ -25307,6 +25262,7 @@ function isPeakMap(value) {
25307
25262
  }
25308
25263
 
25309
25264
  // src/player/components/thumbnailUtils.ts
25265
+ import { decodedUrlPath } from "@hyperframes/parsers";
25310
25266
  function quantizeThumbnailFrameCount(frameCount) {
25311
25267
  const safeCount = Math.max(1, Number.isFinite(frameCount) ? Math.ceil(frameCount) : 1);
25312
25268
  const cap = 2 ** Math.floor(Math.log2(MAX_VISIBLE_THUMBNAIL_FRAMES));
@@ -25357,6 +25313,9 @@ function computeThumbnailStrip(containerWidth, aspect, clipHeight, minFrameWidth
25357
25313
  const frameCount = measured ? Math.max(1, Math.ceil(containerWidth / frameW)) : 1;
25358
25314
  return { frameW, frameCount };
25359
25315
  }
25316
+ function authoredSrcPath(src) {
25317
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(src) ? src : decodedUrlPath(src);
25318
+ }
25360
25319
  function encodePreviewPath(relativePath) {
25361
25320
  return relativePath.split("/").map(encodeURIComponent).join("/");
25362
25321
  }
@@ -25401,7 +25360,7 @@ function loadPeakMap(url) {
25401
25360
  function clipPeaksUrl(src, projectId) {
25402
25361
  const origin2 = window.location.origin;
25403
25362
  const relative = resolvePreviewRelative(
25404
- resolveMediaPreviewUrl(src ?? "", projectId, origin2),
25363
+ resolveMediaPreviewUrl(authoredSrcPath(src ?? ""), projectId, origin2),
25405
25364
  projectId,
25406
25365
  origin2
25407
25366
  );
@@ -25472,7 +25431,7 @@ function isNormalizePlan(value) {
25472
25431
  async function requestNormalizePlan(projectId, el, fetchImpl = studioApiFetch) {
25473
25432
  const origin2 = window.location.origin;
25474
25433
  const relative = resolvePreviewRelative(
25475
- resolveMediaPreviewUrl(el.src ?? "", projectId, origin2),
25434
+ resolveMediaPreviewUrl(authoredSrcPath(el.src ?? ""), projectId, origin2),
25476
25435
  projectId,
25477
25436
  origin2
25478
25437
  );
@@ -26986,10 +26945,10 @@ function formatAttrs(attrs) {
26986
26945
  function readAuthoredSrc(source, target) {
26987
26946
  const tag = findTagByTarget(source, target);
26988
26947
  if (!tag) return "";
26989
- const own = /\bsrc=(["'])([^"']*)\1/.exec(tag.tag);
26990
- if (own?.[2]) return unescapeHtmlAttribute(own[2]);
26991
- const firstSource = /^\s*<source\b[^>]*?\bsrc=(["'])([^"']*)\1/i.exec(source.slice(tag.end + 1));
26992
- return firstSource?.[2] ? unescapeHtmlAttribute(firstSource[2]) : "";
26948
+ const own = readTagAttribute(tag.tag, "src");
26949
+ if (own) return own;
26950
+ const firstSource = /^\s*<source\b[^>]*>/i.exec(source.slice(tag.end + 1));
26951
+ return firstSource && readTagAttribute(firstSource[0], "src") || "";
26993
26952
  }
26994
26953
  function insertBeforeTarget(source, target, markup) {
26995
26954
  const match = findTagByTarget(source, target);
@@ -29960,9 +29919,10 @@ function buildStandaloneRootTimelineElement(params) {
29960
29919
 
29961
29920
  // src/player/hooks/timelineSyncHydration.ts
29962
29921
  import { resolveCssStackingContextId as resolveCssStackingContextId2 } from "@hyperframes/core/runtime/stacking-context";
29922
+ import { LOOP_INFLATED_TIMELINE_SECONDS } from "@hyperframes/core/runtime/composition-length";
29963
29923
  import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR3 } from "@hyperframes/core/audio-groups";
29964
29924
  function sanitizeDurationSeconds(value) {
29965
- return Number.isFinite(value) && value > 0 && value < 7200 ? value : 0;
29925
+ return Number.isFinite(value) && value > 0 && value < LOOP_INFLATED_TIMELINE_SECONDS ? value : 0;
29966
29926
  }
29967
29927
  function readChildAudioGroupState(child) {
29968
29928
  const audioGroup = child.getAttribute(HF_AUDIO_GROUP_ATTR3);
@@ -31340,7 +31300,7 @@ function readParentFlexBasisPixels(element, size) {
31340
31300
  const display2 = readStyleOrComputed(parent, "display").trim();
31341
31301
  if (display2 !== "flex" && display2 !== "inline-flex") return null;
31342
31302
  const direction = readStyleOrComputed(parent, "flex-direction").trim();
31343
- return roundToLayoutPx(Math.max(1, direction.startsWith("column") ? size.height : size.width));
31303
+ return direction.startsWith("column") ? size.height : size.width;
31344
31304
  }
31345
31305
  function restoreStaleStudioScaleResize(element) {
31346
31306
  if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
@@ -31403,17 +31363,16 @@ function writeStudioBoxSizeVars(element, size) {
31403
31363
  element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
31404
31364
  }
31405
31365
  element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
31406
- element.style.setProperty(STUDIO_WIDTH_PROP, `${roundToLayoutPx(Math.max(1, size.width))}px`);
31407
- element.style.setProperty(STUDIO_HEIGHT_PROP, `${roundToLayoutPx(Math.max(1, size.height))}px`);
31366
+ element.style.setProperty(STUDIO_WIDTH_PROP, `${size.width}px`);
31367
+ element.style.setProperty(STUDIO_HEIGHT_PROP, `${size.height}px`);
31408
31368
  }
31409
- function applyStudioBoxSizeDimensions(element, size) {
31369
+ function applyStudioBoxSizeDimensions(element, wantedSize) {
31370
+ const size = roundToLayoutSize(wantedSize);
31410
31371
  writeStudioBoxSizeVars(element, size);
31411
31372
  restoreStaleStudioScaleResize(element);
31412
- const width = roundToLayoutPx(Math.max(1, size.width));
31413
- const height = roundToLayoutPx(Math.max(1, size.height));
31414
31373
  element.style.setProperty("box-sizing", "border-box");
31415
- element.style.setProperty("width", `${width}px`);
31416
- element.style.setProperty("height", `${height}px`);
31374
+ element.style.setProperty("width", `${size.width}px`);
31375
+ element.style.setProperty("height", `${size.height}px`);
31417
31376
  element.style.setProperty("min-width", "0px");
31418
31377
  element.style.setProperty("min-height", "0px");
31419
31378
  element.style.setProperty("max-width", "none");
@@ -31428,6 +31387,7 @@ function applyStudioBoxSizeDimensions(element, size) {
31428
31387
  if (computedDisplay === "inline") {
31429
31388
  element.style.setProperty("display", "inline-block");
31430
31389
  }
31390
+ return size;
31431
31391
  }
31432
31392
  function applyStudioBoxSize(element, size) {
31433
31393
  promoteInlineForTransform(element);
@@ -31445,7 +31405,7 @@ function applyStudioBoxSizeDraft(element, size) {
31445
31405
  boxSizeDraftBases.set(element, { width: element.offsetWidth, height: element.offsetHeight });
31446
31406
  }
31447
31407
  promoteInlineForTransform(element);
31448
- applyStudioBoxSizeDimensions(element, size);
31408
+ return applyStudioBoxSizeDimensions(element, size);
31449
31409
  }
31450
31410
  function isSimpleRotateAngle(value) {
31451
31411
  return /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
@@ -32184,7 +32144,7 @@ function getCachedProbe(url) {
32184
32144
  return cached5?.result;
32185
32145
  }
32186
32146
  function resolveProbeSource(src, projectId) {
32187
- return projectId ? resolveMediaPreviewUrl(src, projectId, window.location.origin) : src;
32147
+ return projectId ? resolveMediaPreviewUrl(authoredSrcPath(src), projectId, window.location.origin) : src;
32188
32148
  }
32189
32149
  function evictMetadataOverflow() {
32190
32150
  const overflow = cache.size + failed.size - TIMELINE_VIEWPORT_BUDGETS.metadataRegistryEntries;
@@ -37615,7 +37575,7 @@ function OffCanvasIndicators({
37615
37575
  const selectOffCanvas = async () => {
37616
37576
  const el = elements.current.get(r.key);
37617
37577
  if (!el) return;
37618
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-KBTPQKGA.js");
37578
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-PYST4G45.js");
37619
37579
  const acp = activeCompositionPathRef.current ?? "index.html";
37620
37580
  const sel = await resolveDomEditSelection2(el, {
37621
37581
  activeCompositionPath: acp,
@@ -37671,7 +37631,7 @@ function ChildRectOutlines({
37671
37631
 
37672
37632
  // src/components/editor/domEditResizeLocal.ts
37673
37633
  var MIN_RESIZE_LOCAL_PX = 1;
37674
- var DEGENERATE_START_DIST_PX = 3;
37634
+ var DEGENERATE_START_DIST_PX = 0.5;
37675
37635
  function resolveCenterResizeScale(input) {
37676
37636
  const startDist = Math.hypot(
37677
37637
  input.pointerStart.x - input.centerStart.x,
@@ -37726,6 +37686,17 @@ function resolveRotatedResizeCursor(handle, rotationDeg) {
37726
37686
  const bucket = Math.round(normalized / 45) % 8;
37727
37687
  return CURSORS_8[bucket];
37728
37688
  }
37689
+ function resizeRemainderShift(corners, grab, { wanted, written }) {
37690
+ const u = { x: corners.ne.x - corners.nw.x, y: corners.ne.y - corners.nw.y };
37691
+ const v = { x: corners.sw.x - corners.nw.x, y: corners.sw.y - corners.nw.y };
37692
+ const det = u.x * v.y - u.y * v.x;
37693
+ if (!det) return { x: 0, y: 0 };
37694
+ const su = Math.sign((grab.x * v.y - grab.y * v.x) / det);
37695
+ const sv = Math.sign((u.x * grab.y - u.y * grab.x) / det);
37696
+ const kw = su * (wanted.width - written.width) / (2 * written.width);
37697
+ const kh = sv * (wanted.height - written.height) / (2 * written.height);
37698
+ return { x: u.x * kw + v.x * kh, y: u.y * kw + v.y * kh };
37699
+ }
37729
37700
  function computeNextResizeAnchor(prev, fixedStart, fixedNow) {
37730
37701
  const base2 = prev ?? { dx: 0, dy: 0 };
37731
37702
  return { dx: base2.dx + (fixedStart.x - fixedNow.x), dy: base2.dy + (fixedStart.y - fixedNow.y) };
@@ -39653,6 +39624,7 @@ function startGesture(kind, e, opts, options) {
39653
39624
  manualEditDragToken,
39654
39625
  snapContext,
39655
39626
  resizeHandle: kind === "resize" ? options?.resizeHandle ?? "se" : void 0,
39627
+ resizePressFromCorner: kind === "resize" && options?.resizeCorner ? { x: e.clientX - options.resizeCorner.x, y: e.clientY - options.resizeCorner.y } : void 0,
39656
39628
  resizeFixedCenterStart
39657
39629
  };
39658
39630
  if (kind === "resize") {
@@ -39746,7 +39718,7 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39746
39718
  offerUndo();
39747
39719
  };
39748
39720
  const drawPressedBox = (origin2) => origin2 && ((dx, dy) => opts.setOverlayRect(shiftedOverlayRect(origin2, dx, dy)));
39749
- const drawResizedBox = (origin2, press) => {
39721
+ const drawResizedBox = (origin2, grabbed) => {
39750
39722
  if (!origin2) return null;
39751
39723
  const bounds = opts.overlayRef.current?.getBoundingClientRect();
39752
39724
  const centre = {
@@ -39754,8 +39726,12 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39754
39726
  y: (bounds?.top ?? 0) + origin2.top + origin2.height / 2
39755
39727
  };
39756
39728
  return (dx, dy) => {
39757
- const pointer = { x: press.x + dx, y: press.y + dy };
39758
- const scale = resolveCenterResizeScale({ pointer, pointerStart: press, centerStart: centre });
39729
+ const pointer = { x: grabbed.x + dx, y: grabbed.y + dy };
39730
+ const scale = resolveCenterResizeScale({
39731
+ pointer,
39732
+ pointerStart: grabbed,
39733
+ centerStart: centre
39734
+ });
39759
39735
  const [width, height] = [origin2.width * scale, origin2.height * scale];
39760
39736
  opts.setOverlayRect({
39761
39737
  ...origin2,
@@ -39874,7 +39850,10 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39874
39850
  let draw = null;
39875
39851
  if (kind === "drag") draw = drawPressedBox(opts.overlayRectRef.current);
39876
39852
  if (kind === "resize")
39877
- draw = drawResizedBox(opts.overlayRectRef.current, { x: e.clientX, y: e.clientY });
39853
+ draw = drawResizedBox(
39854
+ opts.overlayRectRef.current,
39855
+ options?.resizeCorner ?? { x: e.clientX, y: e.clientY }
39856
+ );
39878
39857
  return startOnShownPreview(
39879
39858
  e,
39880
39859
  () => {
@@ -39889,6 +39868,7 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39889
39868
  selection,
39890
39869
  rect,
39891
39870
  resizeHandle: options?.resizeHandle,
39871
+ resizeCorner: options?.resizeCorner,
39892
39872
  at
39893
39873
  });
39894
39874
  },
@@ -39899,10 +39879,14 @@ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGestur
39899
39879
  }
39900
39880
 
39901
39881
  // src/components/editor/resizeDraft.ts
39902
- function resolveResizeAnchor(g, corners, measureOrientedRect) {
39882
+ function resolveResizeAnchor(g, corners, measureOrientedRect, sizes) {
39903
39883
  const fixedStart = g.resizeFixedCenterStart;
39904
39884
  if (corners && fixedStart) {
39905
- return computeNextResizeAnchor(g.lastResizeAnchor, fixedStart, overlayCornersCentroid(corners));
39885
+ const press = g.resizePressFromCorner ?? { x: 0, y: 0 };
39886
+ const grab = { x: g.startX - press.x - g.centerX, y: g.startY - press.y - g.centerY };
39887
+ const shift = resizeRemainderShift(corners, grab, sizes);
39888
+ const target = { x: fixedStart.x + shift.x, y: fixedStart.y + shift.y };
39889
+ return computeNextResizeAnchor(g.lastResizeAnchor, target, overlayCornersCentroid(corners));
39906
39890
  }
39907
39891
  const fallbackRect = measureOrientedRect();
39908
39892
  return resolveResizeCenterAnchorOffset({
@@ -39912,9 +39896,9 @@ function resolveResizeAnchor(g, corners, measureOrientedRect) {
39912
39896
  overlayHeight: fallbackRect ? fallbackRect.height : g.originHeight
39913
39897
  });
39914
39898
  }
39915
- function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect) {
39899
+ function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect, sizes) {
39916
39900
  const corners = overlayEl && iframe ? elementCornerOverlayPoints(overlayEl, iframe, element) : null;
39917
- const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect);
39901
+ const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect, sizes);
39918
39902
  g.lastResizeAnchor = anchor2;
39919
39903
  applyManualOffsetDragDraft(member, anchor2.dx, anchor2.dy);
39920
39904
  return measureOrientedRect() ?? {
@@ -39927,7 +39911,7 @@ function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measu
39927
39911
  angle: g.actualRotation
39928
39912
  };
39929
39913
  }
39930
- function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect) {
39914
+ function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect, sizes) {
39931
39915
  if (g.pathOffsetMember) {
39932
39916
  return resolveAnchoredResizeDraft(
39933
39917
  g,
@@ -39935,7 +39919,8 @@ function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRe
39935
39919
  element,
39936
39920
  overlayEl,
39937
39921
  iframe,
39938
- measureOrientedRect
39922
+ measureOrientedRect,
39923
+ sizes
39939
39924
  );
39940
39925
  }
39941
39926
  return measureOrientedRect() ?? {
@@ -40260,14 +40245,15 @@ function createDomEditOverlayGestureHandlers(opts) {
40260
40245
  }
40261
40246
  if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
40262
40247
  } else {
40248
+ const grab = g.resizePressFromCorner ?? { x: 0, y: 0 };
40263
40249
  const nextSize = resolveCenterResizeSize({
40264
40250
  baseWidth: g.actualWidth,
40265
40251
  baseHeight: g.actualHeight,
40266
- pointer: { x: e.clientX, y: e.clientY },
40267
- pointerStart: { x: g.startX, y: g.startY },
40252
+ pointer: { x: e.clientX - grab.x, y: e.clientY - grab.y },
40253
+ pointerStart: { x: g.startX - grab.x, y: g.startY - grab.y },
40268
40254
  centerStart: { x: g.centerX, y: g.centerY }
40269
40255
  });
40270
- applyStudioBoxSizeDraft(sel.element, nextSize);
40256
+ const writtenSize = applyStudioBoxSizeDraft(sel.element, nextSize);
40271
40257
  const overlayEl = opts.overlayRef.current;
40272
40258
  const iframe = opts.iframeRef.current;
40273
40259
  const measureOrientedRect = () => overlayEl && iframe ? orientedOverlayRect(overlayEl, iframe, sel.element) : null;
@@ -40276,7 +40262,8 @@ function createDomEditOverlayGestureHandlers(opts) {
40276
40262
  sel.element,
40277
40263
  overlayEl,
40278
40264
  iframe,
40279
- measureOrientedRect
40265
+ measureOrientedRect,
40266
+ { wanted: nextSize, written: writtenSize }
40280
40267
  );
40281
40268
  logResizeMove({
40282
40269
  pointer: { x: e.clientX, y: e.clientY },
@@ -41353,13 +41340,22 @@ var RESIZE_HANDLE_DEFS = [
41353
41340
  { handle: "se", cursor: "nwse-resize", x: "right", y: "bottom" }
41354
41341
  ];
41355
41342
  var NO_CROP_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
41356
- function resizeHandleStyle(def, overlayRect, cropInset) {
41343
+ function resizeHandleLayout(def, overlayRect, cropInset) {
41357
41344
  const half = RESIZE_HANDLE_HIT_PX / 2;
41358
41345
  const inset = cropInset ?? NO_CROP_INSET;
41359
- const style = { cursor: def.cursor, touchAction: "none" };
41360
- style.left = def.x === "left" ? overlayRect.left + inset.left - half : overlayRect.left + overlayRect.width - inset.right - half;
41361
- style.top = def.y === "top" ? overlayRect.top + inset.top - half : overlayRect.top + overlayRect.height - inset.bottom - half;
41362
- return style;
41346
+ const cornerX = def.x === "left" ? overlayRect.left + inset.left : overlayRect.left + overlayRect.width - inset.right;
41347
+ const cornerY = def.y === "top" ? overlayRect.top + inset.top : overlayRect.top + overlayRect.height - inset.bottom;
41348
+ const slideOutToFreeMiddleX = (def.x === "left" ? -1 : 1) * Math.max(0, half - (overlayRect.width - inset.left - inset.right) / 4);
41349
+ const slideOutToFreeMiddleY = (def.y === "top" ? -1 : 1) * Math.max(0, half - (overlayRect.height - inset.top - inset.bottom) / 4);
41350
+ return {
41351
+ style: {
41352
+ cursor: def.cursor,
41353
+ touchAction: "none",
41354
+ left: cornerX - half + slideOutToFreeMiddleX,
41355
+ top: cornerY - half + slideOutToFreeMiddleY
41356
+ },
41357
+ anchor: { left: half - slideOutToFreeMiddleX, top: half - slideOutToFreeMiddleY }
41358
+ };
41363
41359
  }
41364
41360
  function DomEditGroupChrome({
41365
41361
  groupOverlayItems,
@@ -41491,26 +41487,42 @@ function DomEditSelectionChrome({
41491
41487
  },
41492
41488
  selectionKey
41493
41489
  ),
41494
- canManipulate && !editing && selection.capabilities.canApplyManualSize && RESIZE_HANDLE_DEFS.map(
41495
- (def) => def.handle !== "se" && !selection.capabilities.canApplyManualOffset ? null : /* @__PURE__ */ jsx99(
41490
+ canManipulate && !editing && selection.capabilities.canApplyManualSize && RESIZE_HANDLE_DEFS.map((def) => {
41491
+ if (def.handle !== "se" && !selection.capabilities.canApplyManualOffset) return null;
41492
+ const layout = resizeHandleLayout(def, overlayRect, cropOutlineInsetPx ?? void 0);
41493
+ return /* @__PURE__ */ jsxs69(
41496
41494
  "div",
41497
41495
  {
41498
41496
  className: "pointer-events-auto absolute flex h-4 w-4 items-center justify-center",
41499
41497
  style: {
41500
- ...resizeHandleStyle(def, overlayRect, cropOutlineInsetPx ?? void 0),
41498
+ ...layout.style,
41501
41499
  // Cursor rotates with the object: bucket the corner's base
41502
41500
  // diagonal + element rotation into the 8 CSS resize cursors.
41503
41501
  cursor: resolveRotatedResizeCursor(def.handle, overlayRect.angle ?? 0)
41504
41502
  },
41505
41503
  onPointerDown: (e) => {
41506
41504
  e.stopPropagation();
41507
- gestures.startGesture("resize", e, { resizeHandle: def.handle });
41505
+ const corner = e.currentTarget.querySelector("[data-resize-corner]")?.getBoundingClientRect();
41506
+ gestures.startGesture("resize", e, {
41507
+ resizeHandle: def.handle,
41508
+ resizeCorner: corner && { x: corner.left, y: corner.top }
41509
+ });
41508
41510
  },
41509
- children: /* @__PURE__ */ jsx99("div", { className: "pointer-events-none h-[12px] w-[12px] rounded-full border-[1.5px] border-studio-accent bg-white shadow-[0_0_3px_rgba(0,0,0,0.45)]" })
41511
+ children: [
41512
+ /* @__PURE__ */ jsx99(
41513
+ "span",
41514
+ {
41515
+ "data-resize-corner": true,
41516
+ className: "pointer-events-none absolute",
41517
+ style: layout.anchor
41518
+ }
41519
+ ),
41520
+ /* @__PURE__ */ jsx99("div", { className: "pointer-events-none h-[12px] w-[12px] rounded-full border-[1.5px] border-studio-accent bg-white shadow-[0_0_3px_rgba(0,0,0,0.45)]" })
41521
+ ]
41510
41522
  },
41511
41523
  def.handle
41512
- )
41513
- )
41524
+ );
41525
+ })
41514
41526
  ]
41515
41527
  }
41516
41528
  ),
@@ -52882,7 +52894,11 @@ function trimFractions(el) {
52882
52894
  return { start, end };
52883
52895
  }
52884
52896
  function renderAudioClip(el, pid, sessionEpoch, labelColor, context, elements, labelInset) {
52885
- const audioUrl = resolveMediaPreviewUrl(el.src ?? "", pid, window.location.origin);
52897
+ const audioUrl = resolveMediaPreviewUrl(
52898
+ authoredSrcPath(el.src ?? ""),
52899
+ pid,
52900
+ window.location.origin
52901
+ );
52886
52902
  const srcRelative = resolvePreviewRelative(audioUrl, pid, window.location.origin);
52887
52903
  const encodedRelative = srcRelative ? encodePreviewPath(srcRelative) : null;
52888
52904
  const waveformUrl = encodedRelative ? buildProjectApiPath(pid, `/waveform/${encodedRelative}`) : void 0;
@@ -52982,7 +52998,11 @@ function useRenderClipContent({
52982
52998
  }
52983
52999
  const htmlPreviewEligible = el.duration > 0 && effectiveTimelineDuration > 0;
52984
53000
  if ((el.tag === "video" || el.tag === "img") && el.src) {
52985
- const mediaSrc = resolveMediaPreviewUrl(el.src, pid, window.location.origin);
53001
+ const mediaSrc = resolveMediaPreviewUrl(
53002
+ authoredSrcPath(el.src),
53003
+ pid,
53004
+ window.location.origin
53005
+ );
52986
53006
  if (el.tag === "img") {
52987
53007
  return createElement(ImageThumbnail, {
52988
53008
  imageSrc: mediaSrc,
@@ -54700,8 +54720,8 @@ function MediaSection({
54700
54720
  const [removeProgress, setRemoveProgress] = useState65(null);
54701
54721
  const [createPlate, setCreatePlate] = useState65(false);
54702
54722
  const [quality, setQuality] = useState65("balanced");
54703
- const absoluteSrc = projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
54704
- const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
54723
+ const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? authoredSrcPath(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
54724
+ const absoluteSrc = projectDir && projectSrc ? `${projectDir}/${projectSrc}` : srcAttr;
54705
54725
  const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
54706
54726
  const panelTitle = isImage ? "Image" : isVideo ? "Video" : "Audio";
54707
54727
  useEffect72(() => {
@@ -56024,7 +56044,7 @@ function resolveProjectAssetPath(sourceFile, src, projectId) {
56024
56044
  return stripPreviewAssetPath(trimmed, projectId);
56025
56045
  }
56026
56046
  const sourceDir = sourceFile.includes("/") ? sourceFile.slice(0, sourceFile.lastIndexOf("/")) : "";
56027
- const parts = `${sourceDir}/${trimmed}`.split("/");
56047
+ const parts = `${sourceDir}/${authoredSrcPath(trimmed)}`.split("/");
56028
56048
  const normalized = [];
56029
56049
  for (const part of parts) {
56030
56050
  if (!part || part === ".") continue;
@@ -58498,6 +58518,7 @@ function ReverseGradientIcon({ size = 16 }) {
58498
58518
  }
58499
58519
 
58500
58520
  // src/components/editor/propertyPanelFill.tsx
58521
+ import { encodeUrlPath as encodeUrlPath3 } from "@hyperframes/parsers";
58501
58522
  import { jsx as jsx137, jsxs as jsxs99 } from "react/jsx-runtime";
58502
58523
  function normalizeProjectPath(value) {
58503
58524
  const trimmed = value.trim();
@@ -58624,7 +58645,7 @@ function ImageFillField({
58624
58645
  selectedAsset && /* @__PURE__ */ jsx137("div", { className: "overflow-hidden rounded-xl border border-neutral-800 bg-neutral-900/80", children: /* @__PURE__ */ jsx137(
58625
58646
  "img",
58626
58647
  {
58627
- src: buildProjectApiPath(projectId, `/preview/${selectedAsset}`),
58648
+ src: buildProjectApiPath(projectId, `/preview/${encodeUrlPath3(selectedAsset)}`),
58628
58649
  alt: selectedAsset.split("/").pop() ?? selectedAsset,
58629
58650
  className: "h-28 w-full object-contain bg-neutral-950/80"
58630
58651
  }
@@ -67090,8 +67111,8 @@ function FlatMediaSection({
67090
67111
  const [removeProgress, setRemoveProgress] = useState94(null);
67091
67112
  const [createPlate, setCreatePlate] = useState94(false);
67092
67113
  const [quality, setQuality] = useState94("balanced");
67093
- const absoluteSrc = projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
67094
- const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
67114
+ const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? authoredSrcPath(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
67115
+ const absoluteSrc = projectDir && projectSrc ? `${projectDir}/${projectSrc}` : srcAttr;
67095
67116
  const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
67096
67117
  useEffect94(() => {
67097
67118
  setRemoveProgress(null);
@@ -75118,6 +75139,7 @@ import { useState as useState117, useCallback as useCallback104, useRef as useRe
75118
75139
 
75119
75140
  // src/hooks/useFileTree.ts
75120
75141
  import { useState as useState116, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo71, useRef as useRef128 } from "react";
75142
+ import { encodeUrlPath as encodeUrlPath4 } from "@hyperframes/parsers";
75121
75143
  var EMPTY_FILE_LIST = [];
75122
75144
  function useFileTree({ projectId, projectIdRef }) {
75123
75145
  const [fetched, setFetched] = useState116(null);
@@ -75189,7 +75211,7 @@ function useFileTree({ projectId, projectIdRef }) {
75189
75211
  () => (projectId ? assets : []).filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
75190
75212
  family: fontFamilyFromAssetPath(asset),
75191
75213
  path: asset,
75192
- url: projectId ? buildProjectApiPath(projectId, `/preview/${asset}`) : ""
75214
+ url: projectId ? buildProjectApiPath(projectId, `/preview/${encodeUrlPath4(asset)}`) : ""
75193
75215
  })),
75194
75216
  [assets, projectId]
75195
75217
  );
@@ -75454,6 +75476,7 @@ function useProjectFileWriter({ projectId }) {
75454
75476
  }
75455
75477
 
75456
75478
  // src/hooks/useFileManager.ts
75479
+ import { encodeUrlPath as encodeUrlPath5 } from "@hyperframes/parsers";
75457
75480
  function useFileManager({
75458
75481
  projectId,
75459
75482
  showToast,
@@ -75763,7 +75786,7 @@ function useFileManager({
75763
75786
  const imported = uploaded.filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
75764
75787
  family: fontFamilyFromAssetPath(asset),
75765
75788
  path: asset,
75766
- url: `/api/projects/${encodeURIComponent(pid)}/preview/${asset}`
75789
+ url: `/api/projects/${encodeURIComponent(pid)}/preview/${encodeUrlPath5(asset)}`
75767
75790
  }));
75768
75791
  importedFontAssetsRef.current = [
75769
75792
  ...imported,
@@ -81799,6 +81822,7 @@ function useDomGeometryCommits({
81799
81822
  }
81800
81823
 
81801
81824
  // src/hooks/useDomEditCommits.ts
81825
+ import { encodeUrlPath as encodeUrlPath6 } from "@hyperframes/parsers";
81802
81826
  function useDomEditCommits({
81803
81827
  activeCompPath,
81804
81828
  previewIframeRef,
@@ -81837,7 +81861,7 @@ function useDomEditCommits({
81837
81861
  return {
81838
81862
  family: fontFamilyFromAssetPath(asset),
81839
81863
  path: asset,
81840
- url: buildProjectApiPath(projectId, `/preview/${asset}`)
81864
+ url: buildProjectApiPath(projectId, `/preview/${encodeUrlPath6(asset)}`)
81841
81865
  };
81842
81866
  },
81843
81867
  [fileTree, projectId, importedFontAssetsRef]
@@ -84815,6 +84839,9 @@ function tweenUsesScaleLonghands(anim) {
84815
84839
  );
84816
84840
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
84817
84841
  }
84842
+ function rectCentre(rect) {
84843
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
84844
+ }
84818
84845
  async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run(), stamp) {
84819
84846
  const moment = editMoment(stamp);
84820
84847
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
@@ -84929,8 +84956,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84929
84956
  scaleDraftEl = el;
84930
84957
  committedScale = useScaleLonghands ? { x: newScaleX, y: newScaleY } : { x: newScaleX, y: newScaleX };
84931
84958
  if (el) {
84932
- const dropRect = draw(() => el.getBoundingClientRect());
84933
- scaleDraftDropPoint = { x: dropRect.x, y: dropRect.y };
84959
+ scaleDraftDropPoint = draw(() => rectCentre(el.getBoundingClientRect()));
84934
84960
  }
84935
84961
  } else {
84936
84962
  resizeProps = {
@@ -84961,31 +84987,31 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84961
84987
  );
84962
84988
  const base3 = { x: baseGsapX, y: baseGsapY };
84963
84989
  setElementGsapPosition(draftEl, base3.x, base3.y);
84964
- const post2 = draftEl.getBoundingClientRect();
84965
- const residual = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
84966
- if (!Number.isFinite(residual.x) || !Number.isFinite(residual.y)) return null;
84967
- if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
84968
- logResize("scale-finalize", { skipped: "already-on-drop-point", residual, base: base3 });
84969
- return "settled";
84970
- }
84990
+ const post2 = rectCentre(draftEl.getBoundingClientRect());
84991
+ const residual2 = { x: dropPoint.x - post2.x, y: dropPoint.y - post2.y };
84992
+ if (!Number.isFinite(residual2.x) || !Number.isFinite(residual2.y)) return null;
84971
84993
  const corrected2 = {
84972
- x: roundTo3(base3.x + residual.x),
84973
- y: roundTo3(base3.y + residual.y)
84994
+ x: roundTo3(base3.x + residual2.x),
84995
+ y: roundTo3(base3.y + residual2.y)
84974
84996
  };
84997
+ if (corrected2.x === roundTo3(base3.x) && corrected2.y === roundTo3(base3.y)) {
84998
+ logResize("scale-finalize", { skipped: "already-on-drop-point", residual: residual2, base: base3 });
84999
+ return "settled";
85000
+ }
84975
85001
  logResize("scale-finalize", {
84976
85002
  dropPoint,
84977
- post: { x: post2.x, y: post2.y },
84978
- residual,
85003
+ post: post2,
85004
+ residual: residual2,
84979
85005
  gsapPos,
84980
85006
  base: base3,
84981
85007
  corrected: corrected2
84982
85008
  });
84983
85009
  setElementGsapPosition(draftEl, corrected2.x, corrected2.y);
84984
- return { base: base3, corrected: corrected2 };
85010
+ return { base: base3, corrected: corrected2, residual: residual2 };
84985
85011
  });
84986
85012
  if (measured === null) return false;
84987
85013
  if (measured === "settled") return true;
84988
- const { base: base2, corrected } = measured;
85014
+ const { base: base2, corrected, residual } = measured;
84989
85015
  const currentAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : resolved2?.animations ?? animations;
84990
85016
  const delta = { x: corrected.x - base2.x, y: corrected.y - base2.y };
84991
85017
  const positionTween = pickClosestToPlayhead(
@@ -84995,6 +85021,10 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
84995
85021
  moment.time
84996
85022
  );
84997
85023
  if (positionTween) {
85024
+ if (Math.abs(residual.x) < 0.5 && Math.abs(residual.y) < 0.5) {
85025
+ draw(() => setElementGsapPosition(draftEl, base2.x, base2.y));
85026
+ return true;
85027
+ }
84998
85028
  logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
84999
85029
  assertGsapEditPersisted(
85000
85030
  await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, {
@@ -91352,6 +91382,7 @@ import { memo as memo50, useCallback as useCallback171 } from "react";
91352
91382
 
91353
91383
  // src/components/MediaPreview.tsx
91354
91384
  import { useState as useState136 } from "react";
91385
+ import { encodeUrlPath as encodeUrlPath7 } from "@hyperframes/parsers";
91355
91386
  import { jsx as jsx212, jsxs as jsxs166 } from "react/jsx-runtime";
91356
91387
  function MediaErrorPanel({ name, filePath }) {
91357
91388
  return /* @__PURE__ */ jsxs166("div", { className: "flex flex-col items-center justify-center h-full p-4 bg-neutral-950 gap-2", children: [
@@ -91379,7 +91410,7 @@ function MediaErrorPanel({ name, filePath }) {
91379
91410
  ] });
91380
91411
  }
91381
91412
  function MediaPreview({ projectId, filePath }) {
91382
- const serveUrl = buildProjectApiPath(projectId, `/preview/${filePath}`);
91413
+ const serveUrl = buildProjectApiPath(projectId, `/preview/${encodeUrlPath7(filePath)}`);
91383
91414
  const name = filePath.split("/").pop() ?? filePath;
91384
91415
  const [failedPath, setFailedPath] = useState136(null);
91385
91416
  const failed2 = failedPath === filePath;