@hyperframes/studio 0.8.102 → 0.8.104

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 (73) hide show
  1. package/dist/assets/{hyperframes-player-BuRiFtrF.js → hyperframes-player-DAoaajuM.js} +1 -1
  2. package/dist/assets/{index-BT3sauee.js → index-BDK4rTMb.js} +1 -1
  3. package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
  4. package/dist/assets/{index-C2utDZt-.js → index-De2aO4XM.js} +179 -179
  5. package/dist/assets/{index-BM35fjFf.js → index-WV9LroAD.js} +1 -1
  6. package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
  7. package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
  8. package/dist/chunk-DI4LCJHL.js.map +1 -0
  9. package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
  10. package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
  11. package/dist/index.d.ts +183 -170
  12. package/dist/index.html +2 -2
  13. package/dist/index.js +550 -389
  14. package/dist/index.js.map +1 -1
  15. package/dist/player.js +2 -2
  16. package/dist/styles.css +1 -1
  17. package/package.json +8 -8
  18. package/src/components/editor/DomEditOverlay.tsx +7 -7
  19. package/src/components/editor/PropertyPanel.tsx +5 -5
  20. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
  21. package/src/components/editor/domEditOverlayGestures.ts +14 -20
  22. package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
  23. package/src/components/editor/domEditOverlayStartGesture.ts +17 -27
  24. package/src/components/editor/manualEdits.test.ts +3 -9
  25. package/src/components/editor/manualEdits.ts +0 -1
  26. package/src/components/editor/manualEditsDom.ts +0 -10
  27. package/src/components/editor/manualEditsDomPatches.test.ts +1 -45
  28. package/src/components/editor/manualEditsDomPatches.ts +1 -23
  29. package/src/components/editor/manualEditsSnapshot.ts +2 -0
  30. package/src/components/editor/manualEditsTypes.ts +2 -0
  31. package/src/components/editor/manualOffsetDrag.test.ts +57 -0
  32. package/src/components/editor/manualOffsetDrag.ts +11 -32
  33. package/src/components/editor/plainTranslate.test.ts +38 -22
  34. package/src/components/editor/plainTranslate.ts +75 -40
  35. package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
  36. package/src/components/editor/previewReadOnlyContext.tsx +3 -1
  37. package/src/components/editor/propertyPanelTransformCommit.test.ts +80 -0
  38. package/src/components/editor/propertyPanelTransformCommit.ts +11 -6
  39. package/src/components/editor/propertyPanelTypes.ts +1 -0
  40. package/src/components/editor/rotateInlineSave.test.ts +111 -0
  41. package/src/components/editor/rotationDraft.test.ts +111 -0
  42. package/src/components/editor/rotationDraft.ts +145 -0
  43. package/src/components/editor/useColorGradingController.test.ts +36 -1
  44. package/src/components/editor/useColorGradingController.ts +2 -2
  45. package/src/components/editor/useDomEditNudge.test.tsx +34 -0
  46. package/src/components/editor/useDomEditNudge.ts +12 -5
  47. package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
  48. package/src/hooks/draggedGsapPosition.ts +28 -0
  49. package/src/hooks/elementOffsetStager.ts +26 -6
  50. package/src/hooks/gsapRuntimeBridge.ts +5 -3
  51. package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
  52. package/src/hooks/plainRotation.test.ts +122 -0
  53. package/src/hooks/plainRotation.ts +42 -0
  54. package/src/hooks/useDomEditSession.ts +5 -5
  55. package/src/hooks/useDomGeometryCommit.test.tsx +75 -0
  56. package/src/hooks/useDomGeometryCommit.ts +17 -17
  57. package/src/hooks/useDomGeometryCommits.test.tsx +56 -1
  58. package/src/hooks/useDomGeometryCommits.ts +28 -36
  59. package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
  60. package/src/hooks/useGsapAwareEditing.ts +43 -42
  61. package/src/hooks/useInlineTextEdit.test.tsx +33 -3
  62. package/src/hooks/useInlineTextEdit.ts +2 -2
  63. package/src/hooks/usePreviewInteraction.test.ts +32 -0
  64. package/src/hooks/usePreviewInteraction.ts +3 -2
  65. package/src/player/components/TimelineClipFades.test.tsx +103 -3
  66. package/src/player/components/TimelineClipFades.tsx +53 -50
  67. package/src/styles/theme.css +1 -1
  68. package/src/utils/projectRouting.test.ts +9 -0
  69. package/src/utils/projectRouting.ts +3 -1
  70. package/dist/chunk-S65AQH6M.js.map +0 -1
  71. /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
  72. /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
  73. /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-C2utDZt-.js";/*!
1
+ import{n as Qi}from"./index-De2aO4XM.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
@@ -2,7 +2,7 @@ import {
2
2
  buildProjectApiPath,
3
3
  getSourceScopedSelectorIndex,
4
4
  withSelectorIndexPass
5
- } from "./chunk-S65AQH6M.js";
5
+ } from "./chunk-DI4LCJHL.js";
6
6
 
7
7
  // src/components/editor/probeSourceElement.ts
8
8
  async function probeSourceElement(projectId, sourceFile, target) {
@@ -1061,4 +1061,4 @@ export {
1061
1061
  readHfId,
1062
1062
  buildDomEditPatchTarget
1063
1063
  };
1064
- //# sourceMappingURL=chunk-MTAVNAGW.js.map
1064
+ //# sourceMappingURL=chunk-46XP7J3B.js.map
@@ -40,8 +40,9 @@ function getSourceScopedSelectorIndex(doc, el, selector, sourceFile, resolveSour
40
40
 
41
41
  // src/utils/projectRouting.ts
42
42
  var PROJECT_HASH_PREFIX = "#project/";
43
+ var WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;
43
44
  function isValidProjectId(value) {
44
- return value.length > 0 && value !== "." && value !== ".." && !value.includes(":") && !value.includes("/") && !value.includes("\\") && !Array.from(value).some((char) => char.charCodeAt(0) < 32);
45
+ return value.length > 0 && value !== "." && value !== ".." && !WINDOWS_DRIVE_PREFIX.test(value) && !value.includes("/") && !value.includes("\\") && !Array.from(value).some((char) => char.charCodeAt(0) < 32);
45
46
  }
46
47
  function decodeHashProjectId(value) {
47
48
  try {
@@ -98,4 +99,4 @@ export {
98
99
  withSelectorIndexPass,
99
100
  getSourceScopedSelectorIndex
100
101
  };
101
- //# sourceMappingURL=chunk-S65AQH6M.js.map
102
+ //# sourceMappingURL=chunk-DI4LCJHL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/sourceScopedSelectorIndex.ts","../src/utils/projectRouting.ts"],"sourcesContent":["/**\n * Occurrence index for a selector within its source document.\n *\n * The preview flattens multiple composition files into one DOM, so a raw\n * `querySelectorAll` index is not a stable source-file identity. Callers supply\n * their existing source resolver; this helper alone owns occurrence scoping.\n */\n\n/** Every element matching one selector, mapped to the source file it belongs to\n * and its occurrence index within that file. One document query fills it. */\ntype SelectorOccurrences = Map<Element, { scope: string; index: number }>;\n\ninterface SelectorIndexPass {\n doc: Document;\n bySelector: Map<string, SelectorOccurrences>;\n}\n\nlet activePass: SelectorIndexPass | null = null;\n\n/**\n * Share one occurrence index per selector across every\n * `getSourceScopedSelectorIndex` call made synchronously inside `run`.\n *\n * Unshared, each call runs its own whole-document `querySelectorAll(selector)`\n * and resolves the source file of every match. A rebuild that walks n elements\n * sharing a selector therefore does n whole-document queries and n² resolver\n * calls — a composition of repeated cards or tiles is exactly that shape. One\n * index per selector makes the same pass linear.\n *\n * Correctness rests on the pass being ONE synchronous walk of a document that\n * does not change under it: nothing invalidates the index, so `run` must not\n * mutate `doc`, and every call inside it must resolve source files the same way\n * (the resolver may be a fresh closure per call, as long as it answers the\n * same). Outside a pass the index is per-call, exactly as before.\n */\nexport function withSelectorIndexPass<T>(doc: Document, run: () => T): T {\n const previous = activePass;\n activePass = { doc, bySelector: new Map() };\n try {\n return run();\n } finally {\n activePass = previous;\n }\n}\n\nfunction buildOccurrences(\n doc: Document,\n selector: string,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): SelectorOccurrences {\n const occurrences: SelectorOccurrences = new Map();\n const counts = new Map<string, number>();\n for (const candidate of doc.querySelectorAll(selector)) {\n const scope = resolveSourceFile(candidate) ?? \"index.html\";\n const index = counts.get(scope) ?? 0;\n counts.set(scope, index + 1);\n occurrences.set(candidate, { scope, index });\n }\n return occurrences;\n}\n\nexport function getSourceScopedSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n sourceFile: string | undefined,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): number | undefined {\n if (!selector || selector.startsWith(\"#\") || selector.startsWith(\"[data-composition-id=\")) {\n return undefined;\n }\n\n try {\n const pass = activePass?.doc === doc ? activePass : null;\n let occurrences = pass?.bySelector.get(selector);\n if (!occurrences) {\n occurrences = buildOccurrences(doc, selector, resolveSourceFile);\n pass?.bySelector.set(selector, occurrences);\n }\n // An element outside the requested scope is not in that scope's occurrence\n // run at all, which is the `indexOf` miss the filtered form returned before.\n const hit = occurrences.get(el);\n return hit && hit.scope === (sourceFile ?? \"index.html\") ? hit.index : undefined;\n } catch {\n return undefined;\n }\n}\n","const PROJECT_HASH_PREFIX = \"#project/\";\n\nexport interface ProjectHashRoute {\n projectId: string;\n params: URLSearchParams;\n}\n\nconst WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;\n\n/** Project names are single path segments, including when received from a hash or server. */\nexport function isValidProjectId(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !WINDOWS_DRIVE_PREFIX.test(value) &&\n !value.includes(\"/\") &&\n !value.includes(\"\\\\\") &&\n !Array.from(value).some((char) => char.charCodeAt(0) < 32)\n );\n}\n\nfunction decodeHashProjectId(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeHashParams(\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): URLSearchParams {\n if (!params) return new URLSearchParams();\n if (params instanceof URLSearchParams) return params;\n\n const next = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (!key || value == null || value === \"\") continue;\n next.set(key, value);\n }\n return next;\n}\n\nexport function encodeProjectId(projectId: string): string {\n if (!isValidProjectId(projectId)) throw new Error(\"Invalid project ID\");\n return encodeURIComponent(projectId);\n}\n\nexport function buildProjectHash(\n projectId: string,\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): string {\n const search = normalizeHashParams(params).toString();\n return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : \"\"}`;\n}\n\nexport function parseProjectHashRoute(hash: string): ProjectHashRoute | null {\n if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;\n\n const route = hash.slice(PROJECT_HASH_PREFIX.length);\n const queryIndex = route.indexOf(\"?\");\n const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;\n if (!encodedProjectId || encodedProjectId.includes(\"/\")) return null;\n\n const projectId = decodeHashProjectId(encodedProjectId);\n if (!isValidProjectId(projectId)) return null;\n\n const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : \"\";\n return {\n projectId,\n params: new URLSearchParams(rawParams),\n };\n}\n\nexport function parseProjectIdFromHash(hash: string): string | null {\n return parseProjectHashRoute(hash)?.projectId ?? null;\n}\n\nexport function buildProjectApiPath(projectId: string, suffix = \"\"): string {\n const normalizedSuffix = suffix && !suffix.startsWith(\"/\") ? `/${suffix}` : suffix;\n return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  buildProjectApiPath,
6
6
  getSourceScopedSelectorIndex
7
- } from "./chunk-S65AQH6M.js";
7
+ } from "./chunk-DI4LCJHL.js";
8
8
 
9
9
  // src/player/lib/time.ts
10
10
  var STUDIO_PREVIEW_FPS = 30;
@@ -1890,4 +1890,4 @@ export {
1890
1890
  whenPreviewBooted,
1891
1891
  Player
1892
1892
  };
1893
- //# sourceMappingURL=chunk-7X4XRP3L.js.map
1893
+ //# sourceMappingURL=chunk-HIGHVWYD.js.map
@@ -20,8 +20,8 @@ import {
20
20
  resolveDomEditCapabilities,
21
21
  resolveDomEditSelection,
22
22
  serializeDomEditTextFields
23
- } from "./chunk-MTAVNAGW.js";
24
- import "./chunk-S65AQH6M.js";
23
+ } from "./chunk-46XP7J3B.js";
24
+ import "./chunk-DI4LCJHL.js";
25
25
  import "./chunk-PZ5AY32C.js";
26
26
  export {
27
27
  buildDefaultDomEditTextField,
@@ -46,4 +46,4 @@ export {
46
46
  resolveDomEditSelection,
47
47
  serializeDomEditTextFields
48
48
  };
49
- //# sourceMappingURL=domEditingLayers-NU6Z5QWA.js.map
49
+ //# sourceMappingURL=domEditingLayers-AQBDFNUC.js.map
package/dist/index.d.ts CHANGED
@@ -2285,6 +2285,8 @@ interface PropertyPanelProps {
2285
2285
  onSetManualOffset: (element: DomEditSelection, next: {
2286
2286
  x: number;
2287
2287
  y: number;
2288
+ }, route?: {
2289
+ plainTranslate: boolean;
2288
2290
  }) => void | Promise<void>;
2289
2291
  onSetManualSize: (element: DomEditSelection, next: {
2290
2292
  width: number;
@@ -2922,24 +2924,150 @@ interface InlineTextEditCommit {
2922
2924
  previousHtml: string;
2923
2925
  }
2924
2926
 
2927
+ /** Where a plain rotate draws its turn, read once at press: the element's own `rotate`, or, when its
2928
+ * transform translates it (often the translate(-50%, -50%) centring), a rotate() right after that
2929
+ * translate, so the box turns in place in screen space. `share` is what the rest already turns. */
2930
+ interface CssRotationTarget {
2931
+ property: "rotate" | "transform";
2932
+ before: string;
2933
+ after: string;
2934
+ share: number;
2935
+ /** -1 when a mirroring `scale` property flips a turn made inside the transform. */
2936
+ sign: number;
2937
+ /** An inline box does not transform, so the turn makes it inline-block. */
2938
+ inline: boolean;
2939
+ }
2940
+ type RotationCommit = {
2941
+ angle: number;
2942
+ plain?: CssRotationTarget;
2943
+ };
2944
+
2945
+ declare const SNAP_THRESHOLD_PX = 6;
2946
+ /** Whether a move of this size has travelled far enough for snapping to apply. */
2947
+ declare function snapEngagedForTravel(dx: number, dy: number): boolean;
2948
+ interface SnapEdge {
2949
+ position: number;
2950
+ source: "grid";
2951
+ id: string;
2952
+ }
2953
+ interface SnapTarget {
2954
+ left: number;
2955
+ top: number;
2956
+ right: number;
2957
+ bottom: number;
2958
+ centerX: number;
2959
+ centerY: number;
2960
+ id: string;
2961
+ }
2962
+ interface SnapGuide {
2963
+ axis: "x" | "y";
2964
+ position: number;
2965
+ /** Extent of the guide line (min of involved elements). */
2966
+ from: number;
2967
+ /** Extent of the guide line (max of involved elements). */
2968
+ to: number;
2969
+ }
2970
+ interface SpacingGuide {
2971
+ axis: "x" | "y";
2972
+ /** Position of the gap (start of gap). */
2973
+ position: number;
2974
+ /** Size of the gap in pixels. */
2975
+ size: number;
2976
+ /** Extent for rendering the indicator. */
2977
+ from: number;
2978
+ /** Extent for rendering the indicator. */
2979
+ to: number;
2980
+ }
2981
+ interface SnapResult {
2982
+ dx: number;
2983
+ dy: number;
2984
+ guides: SnapGuide[];
2985
+ spacingGuides: SpacingGuide[];
2986
+ }
2987
+ interface Rect {
2988
+ left: number;
2989
+ top: number;
2990
+ width: number;
2991
+ height: number;
2992
+ }
2925
2993
  /**
2926
- * Why a DOM edit commit did not change the file.
2927
- *
2928
- * `runDomEditCommit` resolves on persist failure by design (see its contract
2929
- * above), so a caller cannot learn whether the write landed by awaiting it — a
2930
- * failed persist and a successful one are indistinguishable. Capture and apply
2931
- * bugs still reject. The human path does not need to ask about handled persist
2932
- * failures, because `onError` already put a toast on screen. A programmatic
2933
- * caller has no screen, so it has to be told.
2994
+ * Convert overlay rects to snap targets with precomputed edges & centers.
2934
2995
  */
2935
- type DomEditCommitDeclineReason = "no-project" | "no-selection" | "geometry-property" | "styles-not-editable" | "not-text-editable" | "preview-stale" | "persist-failed";
2936
- type DomEditCommitOutcome = {
2937
- ok: true;
2938
- persistence?: DomEditPersistOutcome;
2939
- } | {
2940
- ok: false;
2941
- reason: DomEditCommitDeclineReason;
2996
+ declare function extractSnapTargets(entries: Array<{
2997
+ rect: Rect;
2998
+ id: string;
2999
+ }>): SnapTarget[];
3000
+ /**
3001
+ * Create a snap target from the composition/overlay boundary.
3002
+ */
3003
+ declare function buildCompositionSnapTarget(rect: Rect): SnapTarget;
3004
+ /**
3005
+ * Generate grid-line snap edges.
3006
+ * `gridSpacing` is in composition pixels; `scale` converts to overlay pixels.
3007
+ * X edges are vertical grid lines; Y edges are horizontal grid lines.
3008
+ */
3009
+ declare function buildGridSnapEdges(compositionRect: Rect, gridSpacing: number, scale: number): {
3010
+ x: SnapEdge[];
3011
+ y: SnapEdge[];
2942
3012
  };
3013
+ declare function resolveSnapAdjustment(input: {
3014
+ movingRect: Rect;
3015
+ proposedDx: number;
3016
+ proposedDy: number;
3017
+ targets: SnapTarget[];
3018
+ gridEdges?: {
3019
+ x: SnapEdge[];
3020
+ y: SnapEdge[];
3021
+ };
3022
+ threshold: number;
3023
+ disabled: boolean;
3024
+ /** Set when the gesture has not travelled far enough for snapping yet. */
3025
+ disabledForTravel?: boolean;
3026
+ lockedAxis?: "x" | "y";
3027
+ }): SnapResult;
3028
+ /**
3029
+ * Full-length guide line spanning the composition: a vertical line (axis "x")
3030
+ * runs the composition's height at the snapped x position; a horizontal line
3031
+ * (axis "y") runs the composition's width. `composition` is the composition
3032
+ * rect in overlay space — guide positions are already overlay-space, so the
3033
+ * line must be offset by the composition's left/top (the canvas is usually
3034
+ * letterboxed inside the overlay).
3035
+ */
3036
+ declare function resolveGuideLineRect(guide: SnapGuide, composition: Rect): Rect;
3037
+ declare function resolveEquidistanceGuides(input: {
3038
+ movingRect: Rect;
3039
+ targets: SnapTarget[];
3040
+ threshold: number;
3041
+ }): SpacingGuide[];
3042
+
3043
+ interface SnapContext {
3044
+ targets: SnapTarget[];
3045
+ compositionTarget: SnapTarget | null;
3046
+ gridEdges: {
3047
+ x: SnapEdge[];
3048
+ y: SnapEdge[];
3049
+ } | null;
3050
+ snapEnabled: boolean;
3051
+ }
3052
+ declare function collectSnapContext(input: {
3053
+ overlayEl: HTMLDivElement;
3054
+ iframe: HTMLIFrameElement;
3055
+ excludeElements: Set<HTMLElement>;
3056
+ }): SnapContext;
3057
+
3058
+ interface SnapGuidesState {
3059
+ guides: SnapGuide[];
3060
+ spacingGuides: SpacingGuide[];
3061
+ }
3062
+ interface SnapGuideOverlayProps {
3063
+ snapGuidesRef: RefObject<SnapGuidesState | null>;
3064
+ /** Composition rect in overlay space — guide lines span exactly this rect. */
3065
+ compositionLeft: number;
3066
+ compositionTop: number;
3067
+ compositionWidth: number;
3068
+ compositionHeight: number;
3069
+ }
3070
+ declare const SnapGuideOverlay: react.NamedExoticComponent<SnapGuideOverlayProps>;
2943
3071
 
2944
3072
  interface ApplyDomSelectionOptions {
2945
3073
  revealPanel?: boolean;
@@ -2974,6 +3102,38 @@ interface PreviewMouseDownOptions {
2974
3102
  hoverSelection?: DomEditSelection | null;
2975
3103
  }
2976
3104
 
3105
+ interface MoveCommitOptions {
3106
+ altKey?: boolean;
3107
+ plainTranslate?: boolean;
3108
+ }
3109
+ interface DomEditGroupPathOffsetCommit {
3110
+ selection: DomEditSelection;
3111
+ next: {
3112
+ x: number;
3113
+ y: number;
3114
+ };
3115
+ plainTranslate?: boolean;
3116
+ }
3117
+
3118
+ /**
3119
+ * Why a DOM edit commit did not change the file.
3120
+ *
3121
+ * `runDomEditCommit` resolves on persist failure by design (see its contract
3122
+ * above), so a caller cannot learn whether the write landed by awaiting it — a
3123
+ * failed persist and a successful one are indistinguishable. Capture and apply
3124
+ * bugs still reject. The human path does not need to ask about handled persist
3125
+ * failures, because `onError` already put a toast on screen. A programmatic
3126
+ * caller has no screen, so it has to be told.
3127
+ */
3128
+ type DomEditCommitDeclineReason = "no-project" | "no-selection" | "geometry-property" | "styles-not-editable" | "not-text-editable" | "preview-stale" | "persist-failed";
3129
+ type DomEditCommitOutcome = {
3130
+ ok: true;
3131
+ persistence?: DomEditPersistOutcome;
3132
+ } | {
3133
+ ok: false;
3134
+ reason: DomEditCommitDeclineReason;
3135
+ };
3136
+
2977
3137
  /**
2978
3138
  * Which elements a delete acts on.
2979
3139
  *
@@ -3056,9 +3216,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
3056
3216
  handleDomPathOffsetCommit: (selection: DomEditSelection, next: {
3057
3217
  x: number;
3058
3218
  y: number;
3059
- }, modifiers?: {
3060
- altKey?: boolean;
3061
- }) => Promise<void>;
3219
+ }, modifiers?: MoveCommitOptions) => Promise<void>;
3062
3220
  handleDomGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void>;
3063
3221
  handleDomZIndexReorderCommit: (entries: Array<{
3064
3222
  element: HTMLElement;
@@ -3076,9 +3234,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
3076
3234
  x: number;
3077
3235
  y: number;
3078
3236
  }, restore?: () => void) => Promise<void>;
3079
- handleDomRotationCommit: (selection: DomEditSelection, next: {
3080
- angle: number;
3081
- }) => Promise<void>;
3237
+ handleDomRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
3082
3238
  handleDomManualEditsReset: (selection: DomEditSelection) => Promise<void>;
3083
3239
  handleDomTextCommit: (value: string, fieldKey?: string) => Promise<DomEditCommitOutcome>;
3084
3240
  handleDomTextCommitForSelection: (selection: DomEditSelection, value: string, fieldKey?: string) => Promise<DomEditCommitOutcome>;
@@ -3089,7 +3245,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
3089
3245
  getGsapAnimationsForSelection: (selection: DomEditSelection) => _hyperframes_parsers.GsapAnimation[] | Promise<_hyperframes_parsers.GsapAnimation[]>;
3090
3246
  handleAskAgent: () => void;
3091
3247
  handleAgentModalSubmit: (userInstruction: string) => Promise<void>;
3092
- handleBlockedDomMove: (selection: DomEditSelection) => void;
3248
+ handleBlockedDomMove: (selection: DomEditSelection, reason?: string) => void;
3093
3249
  handleDomManualDragStart: () => void;
3094
3250
  handleDomEditElementDelete: (selection: DomEditSelection, options?: {
3095
3251
  expandGroup?: boolean;
@@ -3244,141 +3400,6 @@ declare function PreviewReadOnlyProvider({ readOnly, reason, children, }: {
3244
3400
  children: ReactNode;
3245
3401
  }): react_jsx_runtime.JSX.Element;
3246
3402
 
3247
- declare const SNAP_THRESHOLD_PX = 6;
3248
- /** Whether a move of this size has travelled far enough for snapping to apply. */
3249
- declare function snapEngagedForTravel(dx: number, dy: number): boolean;
3250
- interface SnapEdge {
3251
- position: number;
3252
- source: "grid";
3253
- id: string;
3254
- }
3255
- interface SnapTarget {
3256
- left: number;
3257
- top: number;
3258
- right: number;
3259
- bottom: number;
3260
- centerX: number;
3261
- centerY: number;
3262
- id: string;
3263
- }
3264
- interface SnapGuide {
3265
- axis: "x" | "y";
3266
- position: number;
3267
- /** Extent of the guide line (min of involved elements). */
3268
- from: number;
3269
- /** Extent of the guide line (max of involved elements). */
3270
- to: number;
3271
- }
3272
- interface SpacingGuide {
3273
- axis: "x" | "y";
3274
- /** Position of the gap (start of gap). */
3275
- position: number;
3276
- /** Size of the gap in pixels. */
3277
- size: number;
3278
- /** Extent for rendering the indicator. */
3279
- from: number;
3280
- /** Extent for rendering the indicator. */
3281
- to: number;
3282
- }
3283
- interface SnapResult {
3284
- dx: number;
3285
- dy: number;
3286
- guides: SnapGuide[];
3287
- spacingGuides: SpacingGuide[];
3288
- }
3289
- interface Rect {
3290
- left: number;
3291
- top: number;
3292
- width: number;
3293
- height: number;
3294
- }
3295
- /**
3296
- * Convert overlay rects to snap targets with precomputed edges & centers.
3297
- */
3298
- declare function extractSnapTargets(entries: Array<{
3299
- rect: Rect;
3300
- id: string;
3301
- }>): SnapTarget[];
3302
- /**
3303
- * Create a snap target from the composition/overlay boundary.
3304
- */
3305
- declare function buildCompositionSnapTarget(rect: Rect): SnapTarget;
3306
- /**
3307
- * Generate grid-line snap edges.
3308
- * `gridSpacing` is in composition pixels; `scale` converts to overlay pixels.
3309
- * X edges are vertical grid lines; Y edges are horizontal grid lines.
3310
- */
3311
- declare function buildGridSnapEdges(compositionRect: Rect, gridSpacing: number, scale: number): {
3312
- x: SnapEdge[];
3313
- y: SnapEdge[];
3314
- };
3315
- declare function resolveSnapAdjustment(input: {
3316
- movingRect: Rect;
3317
- proposedDx: number;
3318
- proposedDy: number;
3319
- targets: SnapTarget[];
3320
- gridEdges?: {
3321
- x: SnapEdge[];
3322
- y: SnapEdge[];
3323
- };
3324
- threshold: number;
3325
- disabled: boolean;
3326
- /** Set when the gesture has not travelled far enough for snapping yet. */
3327
- disabledForTravel?: boolean;
3328
- lockedAxis?: "x" | "y";
3329
- }): SnapResult;
3330
- /**
3331
- * Full-length guide line spanning the composition: a vertical line (axis "x")
3332
- * runs the composition's height at the snapped x position; a horizontal line
3333
- * (axis "y") runs the composition's width. `composition` is the composition
3334
- * rect in overlay space — guide positions are already overlay-space, so the
3335
- * line must be offset by the composition's left/top (the canvas is usually
3336
- * letterboxed inside the overlay).
3337
- */
3338
- declare function resolveGuideLineRect(guide: SnapGuide, composition: Rect): Rect;
3339
- declare function resolveEquidistanceGuides(input: {
3340
- movingRect: Rect;
3341
- targets: SnapTarget[];
3342
- threshold: number;
3343
- }): SpacingGuide[];
3344
-
3345
- interface SnapContext {
3346
- targets: SnapTarget[];
3347
- compositionTarget: SnapTarget | null;
3348
- gridEdges: {
3349
- x: SnapEdge[];
3350
- y: SnapEdge[];
3351
- } | null;
3352
- snapEnabled: boolean;
3353
- }
3354
- declare function collectSnapContext(input: {
3355
- overlayEl: HTMLDivElement;
3356
- iframe: HTMLIFrameElement;
3357
- excludeElements: Set<HTMLElement>;
3358
- }): SnapContext;
3359
-
3360
- interface SnapGuidesState {
3361
- guides: SnapGuide[];
3362
- spacingGuides: SpacingGuide[];
3363
- }
3364
- interface SnapGuideOverlayProps {
3365
- snapGuidesRef: RefObject<SnapGuidesState | null>;
3366
- /** Composition rect in overlay space — guide lines span exactly this rect. */
3367
- compositionLeft: number;
3368
- compositionTop: number;
3369
- compositionWidth: number;
3370
- compositionHeight: number;
3371
- }
3372
- declare const SnapGuideOverlay: react.NamedExoticComponent<SnapGuideOverlayProps>;
3373
-
3374
- interface DomEditGroupPathOffsetCommit {
3375
- selection: DomEditSelection;
3376
- next: {
3377
- x: number;
3378
- y: number;
3379
- };
3380
- }
3381
-
3382
3403
  /**
3383
3404
  * Pure z-order helpers for the canvas right-click context menu.
3384
3405
  *
@@ -3450,14 +3471,12 @@ interface DomEditOverlayProps {
3450
3471
  revealPanel?: boolean;
3451
3472
  additive?: boolean;
3452
3473
  }) => void;
3453
- onBlockedMove: (selection: DomEditSelection) => void;
3474
+ onBlockedMove: (selection: DomEditSelection, reason?: string) => void;
3454
3475
  onManualDragStart?: () => void;
3455
3476
  onPathOffsetCommit: (selection: DomEditSelection, next: {
3456
3477
  x: number;
3457
3478
  y: number;
3458
- }, modifiers?: {
3459
- altKey?: boolean;
3460
- }) => Promise<unknown> | void;
3479
+ }, modifiers?: MoveCommitOptions) => Promise<unknown> | void;
3461
3480
  onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void;
3462
3481
  onBoxSizeCommit: (selection: DomEditSelection, next: {
3463
3482
  width: number;
@@ -3466,9 +3485,7 @@ interface DomEditOverlayProps {
3466
3485
  x: number;
3467
3486
  y: number;
3468
3487
  }, restore?: () => void) => Promise<unknown> | void;
3469
- onRotationCommit: (selection: DomEditSelection, next: {
3470
- angle: number;
3471
- }) => Promise<unknown> | void;
3488
+ onRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<unknown> | void;
3472
3489
  onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
3473
3490
  recordingState?: GestureRecordingState;
3474
3491
  onToggleRecording?: () => void;
@@ -3552,9 +3569,7 @@ interface DomGeometryCommits {
3552
3569
  commitPathOffset: (selection: DomEditSelection, next: {
3553
3570
  x: number;
3554
3571
  y: number;
3555
- }, modifiers?: {
3556
- altKey?: boolean;
3557
- }) => Promise<DomEditCommitOutcome>;
3572
+ }, modifiers?: MoveCommitOptions) => Promise<DomEditCommitOutcome>;
3558
3573
  commitGroupPathOffset: (updates: DomEditGroupPathOffsetCommit[]) => Promise<DomEditCommitOutcome>;
3559
3574
  commitBoxSize: (selection: DomEditSelection, next: {
3560
3575
  width: number;
@@ -3563,9 +3578,7 @@ interface DomGeometryCommits {
3563
3578
  x: number;
3564
3579
  y: number;
3565
3580
  }, restore?: () => void) => Promise<DomEditCommitOutcome>;
3566
- commitRotation: (selection: DomEditSelection, next: {
3567
- angle: number;
3568
- }) => Promise<DomEditCommitOutcome>;
3581
+ commitRotation: (selection: DomEditSelection, next: RotationCommit) => Promise<DomEditCommitOutcome>;
3569
3582
  waitForPendingSaves: () => Promise<void>;
3570
3583
  }
3571
3584
  /**
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-C2utDZt-.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-xB5O0hPw.css">
8
+ <script type="module" crossorigin src="/assets/index-De2aO4XM.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CQN3M56N.css">
10
10
  </head>
11
11
  <body>
12
12
  <div data-hf-id="hf-aph5" id="root"></div>