@hyperframes/studio 0.7.21 → 0.7.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-jbPe1Dih.js → index-B4h4u7eW.js} +152 -152
- package/dist/assets/{index-gFA786gK.js → index-B_gDTiNI.js} +1 -1
- package/dist/assets/{index-pAaVqALC.js → index-gk_X4nXD.js} +1 -1
- package/dist/{chunk-SBGXX7WY.js → chunk-AN2EWWK3.js} +59 -108
- package/dist/chunk-AN2EWWK3.js.map +1 -0
- package/dist/{domEditingLayers-VZMLL4AP.js → domEditingLayers-EK7R7R4G.js} +4 -2
- package/dist/index.html +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -5
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/editor/PropertyPanel.tsx +11 -8
- package/src/components/editor/domEditingDom.ts +4 -33
- package/src/components/editor/domEditingLayers.ts +88 -108
- package/src/components/editor/manualEditingAvailability.ts +0 -10
- package/src/components/editor/propertyPanelColorGradingSection.tsx +0 -4
- package/src/components/editor/propertyPanelMediaSection.tsx +0 -6
- package/src/contexts/ViewModeContext.tsx +10 -19
- package/src/hooks/useDomEditSession.ts +3 -1
- package/src/utils/sdkResolverShadow.test.ts +47 -0
- package/src/utils/sdkResolverShadow.ts +35 -2
- package/dist/chunk-SBGXX7WY.js.map +0 -1
- /package/dist/{domEditingLayers-VZMLL4AP.js.map → domEditingLayers-EK7R7R4G.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.22",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/core": "0.7.22",
|
|
50
|
+
"@hyperframes/parsers": "0.7.22",
|
|
51
|
+
"@hyperframes/sdk": "0.7.22",
|
|
52
|
+
"@hyperframes/player": "0.7.22",
|
|
53
|
+
"@hyperframes/studio-server": "0.7.22"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.22"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -36,10 +36,7 @@ import {
|
|
|
36
36
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
37
37
|
import { StudioHeader } from "./components/StudioHeader";
|
|
38
38
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
39
|
-
import {
|
|
40
|
-
STUDIO_KEYFRAMES_ENABLED,
|
|
41
|
-
STUDIO_STORYBOARD_ENABLED,
|
|
42
|
-
} from "./components/editor/manualEditingAvailability";
|
|
39
|
+
import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
|
|
43
40
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
44
41
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
45
42
|
import { StudioPreviewArea } from "./components/StudioPreviewArea";
|
|
@@ -65,7 +62,7 @@ type CanvasRect = { left: number; top: number; width: number; height: number };
|
|
|
65
62
|
export function StudioApp() {
|
|
66
63
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
67
64
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
68
|
-
const viewModeValue = useViewModeState(
|
|
65
|
+
const viewModeValue = useViewModeState();
|
|
69
66
|
|
|
70
67
|
// sessionStorage-backed: fires once per tab, survives HMR remounts
|
|
71
68
|
useEffect(() => {
|
|
@@ -3,7 +3,6 @@ import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
|
|
|
3
3
|
import {
|
|
4
4
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
5
5
|
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
6
|
-
STUDIO_STORYBOARD_ENABLED,
|
|
7
6
|
} from "./editor/manualEditingAvailability";
|
|
8
7
|
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
9
8
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
@@ -204,8 +203,8 @@ export function StudioHeader({
|
|
|
204
203
|
</span>
|
|
205
204
|
<span className="text-[11px] font-medium text-neutral-300">{projectId}</span>
|
|
206
205
|
</div>
|
|
207
|
-
{/* Center: storyboard / preview toggle
|
|
208
|
-
|
|
206
|
+
{/* Center: storyboard / preview toggle */}
|
|
207
|
+
<ViewModeToggle />
|
|
209
208
|
{/* Right: toolbar buttons */}
|
|
210
209
|
<div className="flex items-center gap-1.5">
|
|
211
210
|
<button
|
|
@@ -14,11 +14,10 @@ import {
|
|
|
14
14
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
|
15
15
|
import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
|
|
16
16
|
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "./propertyPanelColorGradingSection";
|
|
17
|
+
import { resolveEditingSections } from "@hyperframes/core/editing";
|
|
18
|
+
import { MediaSection } from "./propertyPanelMediaSection";
|
|
19
|
+
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
|
20
|
+
import { domEditSelectionToFacts } from "./domEditingLayers";
|
|
22
21
|
import { TextSection, StyleSections } from "./propertyPanelSections";
|
|
23
22
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
24
23
|
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
|
|
@@ -199,6 +198,10 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
199
198
|
const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
|
|
200
199
|
const sourceLabel = element.id ? `#${element.id}` : element.selector;
|
|
201
200
|
const showEditableSections = element.capabilities.canEditStyles;
|
|
201
|
+
// Capabilities are already resolved on the selection; recompute only sections,
|
|
202
|
+
// feeding the live GSAP tween count (arrives on the gsapAnimations prop, not the
|
|
203
|
+
// selection) so the Timing section shows for pure-GSAP elements with no data-start.
|
|
204
|
+
const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
|
|
202
205
|
const manualOffset = readStudioPathOffset(element.element);
|
|
203
206
|
const manualSize = readStudioBoxSize(element.element);
|
|
204
207
|
const resolvedWidth =
|
|
@@ -339,7 +342,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
339
342
|
onRemoveTextField={onRemoveTextField}
|
|
340
343
|
/>
|
|
341
344
|
|
|
342
|
-
{
|
|
345
|
+
{sections.timing && (
|
|
343
346
|
// Render whenever there's an authored clip range OR animations to infer
|
|
344
347
|
// one from — a pure-GSAP element with no data-start still gets a Timing
|
|
345
348
|
// range (TimingSection derives it from its tweens).
|
|
@@ -349,7 +352,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
349
352
|
onSetAttribute={onSetAttribute}
|
|
350
353
|
/>
|
|
351
354
|
)}
|
|
352
|
-
{
|
|
355
|
+
{sections.media && (
|
|
353
356
|
<MediaSection
|
|
354
357
|
projectDir={projectDir}
|
|
355
358
|
element={element}
|
|
@@ -360,7 +363,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
360
363
|
/>
|
|
361
364
|
)}
|
|
362
365
|
|
|
363
|
-
{STUDIO_COLOR_GRADING_ENABLED &&
|
|
366
|
+
{STUDIO_COLOR_GRADING_ENABLED && sections.colorGrading && (
|
|
364
367
|
<ColorGradingSection
|
|
365
368
|
key={[
|
|
366
369
|
element.id ?? "",
|
|
@@ -19,39 +19,10 @@ export function isHtmlElement(value: unknown): value is HTMLElement {
|
|
|
19
19
|
|
|
20
20
|
// ─── Style parsing ────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const parsed = parseFloat(trimmed);
|
|
27
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isIdentityTransform(value: string | undefined): boolean {
|
|
31
|
-
const transform = (value ?? "none").trim();
|
|
32
|
-
if (!transform || transform === "none") return true;
|
|
33
|
-
|
|
34
|
-
const matrix = transform.match(/^matrix\(([^)]+)\)$/i);
|
|
35
|
-
if (matrix) {
|
|
36
|
-
const values = matrix[1].split(",").map((part) => Number.parseFloat(part.trim()));
|
|
37
|
-
if (values.length !== 6 || values.some((part) => !Number.isFinite(part))) return false;
|
|
38
|
-
return (
|
|
39
|
-
Math.abs(values[0] - 1) < 0.0001 &&
|
|
40
|
-
Math.abs(values[1]) < 0.0001 &&
|
|
41
|
-
Math.abs(values[2]) < 0.0001 &&
|
|
42
|
-
Math.abs(values[3] - 1) < 0.0001 &&
|
|
43
|
-
Math.abs(values[4]) < 0.0001 &&
|
|
44
|
-
Math.abs(values[5]) < 0.0001
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const matrix3d = transform.match(/^matrix3d\(([^)]+)\)$/i);
|
|
49
|
-
if (!matrix3d) return false;
|
|
50
|
-
const values = matrix3d[1].split(",").map((part) => Number.parseFloat(part.trim()));
|
|
51
|
-
if (values.length !== 16 || values.some((part) => !Number.isFinite(part))) return false;
|
|
52
|
-
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
53
|
-
return values.every((part, index) => Math.abs(part - identity[index]) < 0.0001);
|
|
54
|
-
}
|
|
22
|
+
// Single source of truth lives in @hyperframes/core/editing so the studio
|
|
23
|
+
// callers and the core resolver can't drift. Re-exported here to keep this
|
|
24
|
+
// module's public surface (6 studio callers import parsePx from it).
|
|
25
|
+
export { parsePx } from "@hyperframes/core/editing";
|
|
55
26
|
|
|
56
27
|
export function isTextBearingTag(tagName: string): boolean {
|
|
57
28
|
return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* for dom editing.
|
|
4
4
|
*/
|
|
5
5
|
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
6
|
+
import {
|
|
7
|
+
resolveEditingAffordances,
|
|
8
|
+
resolveEditingSections,
|
|
9
|
+
type EditableElementFacts,
|
|
10
|
+
} from "@hyperframes/core/editing";
|
|
6
11
|
import { groupScopedLayerRoots, resolveGroupCapture } from "./domEditingGroups";
|
|
7
12
|
import type {
|
|
8
13
|
DomEditCapabilities,
|
|
@@ -21,9 +26,7 @@ import {
|
|
|
21
26
|
getSelectorIndex,
|
|
22
27
|
getSourceFileForElement,
|
|
23
28
|
isHtmlElement,
|
|
24
|
-
isIdentityTransform,
|
|
25
29
|
isTextBearingTag,
|
|
26
|
-
parsePx,
|
|
27
30
|
} from "./domEditingDom";
|
|
28
31
|
import {
|
|
29
32
|
findElementForSelection,
|
|
@@ -171,12 +174,66 @@ export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>):
|
|
|
171
174
|
|
|
172
175
|
// ─── Capabilities ────────────────────────────────────────────────────────────
|
|
173
176
|
|
|
174
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Build the geometry/capability half of EditableElementFacts. Section inputs
|
|
179
|
+
* (text/timing/animation) are irrelevant to capability resolution, so they are
|
|
180
|
+
* zeroed here. Shared by the wrapper and the live-selection path so the two
|
|
181
|
+
* fact-construction sites can't disagree.
|
|
182
|
+
*/
|
|
183
|
+
function capabilityFacts(geometry: {
|
|
184
|
+
hasStableTarget: boolean;
|
|
185
|
+
tag: string;
|
|
186
|
+
inlineStyles: Record<string, string>;
|
|
187
|
+
computedStyles: Record<string, string>;
|
|
188
|
+
isCompositionHost: boolean;
|
|
189
|
+
isCompositionRoot: boolean;
|
|
190
|
+
isInsideLockedComposition: boolean;
|
|
191
|
+
isMasterView: boolean;
|
|
192
|
+
existsInSource: boolean;
|
|
193
|
+
}): EditableElementFacts {
|
|
194
|
+
return {
|
|
195
|
+
...geometry,
|
|
196
|
+
hasEditableText: false,
|
|
197
|
+
hasTimingStart: false,
|
|
198
|
+
animationCount: 0,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build core EditableElementFacts from a fully-resolved DomEditSelection.
|
|
204
|
+
* `animationCount` is supplied by the caller because live GSAP tweens arrive on
|
|
205
|
+
* a separate channel (the PropertyPanel `gsapAnimations` prop), not on the
|
|
206
|
+
* selection — `selection.gsapAnimations` is never populated.
|
|
207
|
+
*/
|
|
208
|
+
export function domEditSelectionToFacts(
|
|
209
|
+
selection: DomEditSelection,
|
|
210
|
+
animationCount = selection.gsapAnimations?.length ?? 0,
|
|
211
|
+
): EditableElementFacts {
|
|
212
|
+
return {
|
|
213
|
+
hasStableTarget: Boolean(selection.selector || selection.hfId),
|
|
214
|
+
tag: selection.tagName,
|
|
215
|
+
inlineStyles: selection.inlineStyles,
|
|
216
|
+
computedStyles: selection.computedStyles,
|
|
217
|
+
isCompositionHost: selection.isCompositionHost,
|
|
218
|
+
isCompositionRoot: false,
|
|
219
|
+
isInsideLockedComposition: selection.isInsideLockedComposition,
|
|
220
|
+
isMasterView: false,
|
|
221
|
+
existsInSource: true,
|
|
222
|
+
hasEditableText: selection.textFields.length > 0,
|
|
223
|
+
hasTimingStart: selection.dataAttributes.start != null,
|
|
224
|
+
animationCount,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolve DOM edit capabilities for a given element.
|
|
230
|
+
* Thin wrapper over core resolveEditingAffordances — kept for backward
|
|
231
|
+
* compatibility (tests and the barrel import this signature directly).
|
|
232
|
+
*/
|
|
175
233
|
export function resolveDomEditCapabilities(args: {
|
|
176
234
|
selector?: string;
|
|
177
235
|
hfId?: string;
|
|
178
236
|
tagName?: string;
|
|
179
|
-
className?: string;
|
|
180
237
|
inlineStyles: Record<string, string>;
|
|
181
238
|
computedStyles: Record<string, string>;
|
|
182
239
|
isCompositionHost: boolean;
|
|
@@ -185,92 +242,19 @@ export function resolveDomEditCapabilities(args: {
|
|
|
185
242
|
isMasterView: boolean;
|
|
186
243
|
existsInSource?: boolean;
|
|
187
244
|
}): DomEditCapabilities {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (args.existsInSource === false) {
|
|
204
|
-
return {
|
|
205
|
-
canSelect: true,
|
|
206
|
-
canEditStyles: false,
|
|
207
|
-
canMove: false,
|
|
208
|
-
canResize: false,
|
|
209
|
-
canApplyManualOffset: false,
|
|
210
|
-
canApplyManualSize: false,
|
|
211
|
-
canApplyManualRotation: false,
|
|
212
|
-
reasonIfDisabled: "This element is generated by a script and cannot be edited visually.",
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (args.isCompositionRoot) {
|
|
217
|
-
return {
|
|
218
|
-
canSelect: true,
|
|
219
|
-
canEditStyles: true,
|
|
220
|
-
canMove: false,
|
|
221
|
-
canResize: false,
|
|
222
|
-
canApplyManualOffset: false,
|
|
223
|
-
canApplyManualSize: false,
|
|
224
|
-
canApplyManualRotation: false,
|
|
225
|
-
reasonIfDisabled: "The root composition defines the preview bounds.",
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const position = args.computedStyles.position;
|
|
230
|
-
const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
|
|
231
|
-
const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
|
|
232
|
-
const width = parsePx(args.inlineStyles.width) ?? parsePx(args.computedStyles.width);
|
|
233
|
-
const height = parsePx(args.inlineStyles.height) ?? parsePx(args.computedStyles.height);
|
|
234
|
-
const hasTransformDrivenGeometry = !isIdentityTransform(args.computedStyles.transform);
|
|
235
|
-
|
|
236
|
-
const canMove =
|
|
237
|
-
(position === "absolute" || position === "fixed") &&
|
|
238
|
-
left != null &&
|
|
239
|
-
top != null &&
|
|
240
|
-
!hasTransformDrivenGeometry;
|
|
241
|
-
|
|
242
|
-
const canResize = canMove && (width != null || height != null);
|
|
243
|
-
const canApplyManualGeometry = !args.isCompositionHost;
|
|
244
|
-
const canApplyManualOffset = canApplyManualGeometry;
|
|
245
|
-
const canApplyManualSize = canApplyManualGeometry;
|
|
246
|
-
const canApplyManualRotation = canApplyManualGeometry;
|
|
247
|
-
const reasonIfDisabled = canApplyManualGeometry
|
|
248
|
-
? undefined
|
|
249
|
-
: "Select an internal layer to transform it.";
|
|
250
|
-
|
|
251
|
-
if (args.isCompositionHost && args.isMasterView) {
|
|
252
|
-
return {
|
|
253
|
-
canSelect: true,
|
|
254
|
-
canEditStyles: false,
|
|
255
|
-
canMove,
|
|
256
|
-
canResize,
|
|
257
|
-
canApplyManualOffset,
|
|
258
|
-
canApplyManualSize,
|
|
259
|
-
canApplyManualRotation,
|
|
260
|
-
reasonIfDisabled,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
canSelect: true,
|
|
266
|
-
canEditStyles: true,
|
|
267
|
-
canMove,
|
|
268
|
-
canResize,
|
|
269
|
-
canApplyManualOffset,
|
|
270
|
-
canApplyManualSize,
|
|
271
|
-
canApplyManualRotation,
|
|
272
|
-
reasonIfDisabled,
|
|
273
|
-
};
|
|
245
|
+
return resolveEditingAffordances(
|
|
246
|
+
capabilityFacts({
|
|
247
|
+
hasStableTarget: Boolean(args.selector || args.hfId),
|
|
248
|
+
tag: (args.tagName ?? "div").toLowerCase(),
|
|
249
|
+
inlineStyles: args.inlineStyles,
|
|
250
|
+
computedStyles: args.computedStyles,
|
|
251
|
+
isCompositionHost: args.isCompositionHost,
|
|
252
|
+
isCompositionRoot: args.isCompositionRoot ?? false,
|
|
253
|
+
isInsideLockedComposition: args.isInsideLockedComposition ?? false,
|
|
254
|
+
isMasterView: args.isMasterView,
|
|
255
|
+
existsInSource: args.existsInSource ?? true,
|
|
256
|
+
}),
|
|
257
|
+
).capabilities;
|
|
274
258
|
}
|
|
275
259
|
|
|
276
260
|
// ─── Element label ────────────────────────────────────────────────────────────
|
|
@@ -354,19 +338,19 @@ export async function resolveDomEditSelection(
|
|
|
354
338
|
if (selectorIndex != null) probeTarget.selectorIndex = selectorIndex;
|
|
355
339
|
existsInSource = await probeSourceElement(options.projectId, sourceFile, probeTarget);
|
|
356
340
|
}
|
|
357
|
-
const capabilities =
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
341
|
+
const capabilities = resolveEditingAffordances(
|
|
342
|
+
capabilityFacts({
|
|
343
|
+
hasStableTarget: Boolean(selector || hfId),
|
|
344
|
+
tag: current.tagName.toLowerCase(),
|
|
345
|
+
inlineStyles,
|
|
346
|
+
computedStyles,
|
|
347
|
+
isCompositionHost: Boolean(compositionSrc),
|
|
348
|
+
isCompositionRoot,
|
|
349
|
+
isInsideLockedComposition: isInsideLocked,
|
|
350
|
+
isMasterView: options.isMasterView,
|
|
351
|
+
existsInSource: existsInSource ?? true,
|
|
352
|
+
}),
|
|
353
|
+
).capabilities;
|
|
370
354
|
const rect = current.getBoundingClientRect();
|
|
371
355
|
|
|
372
356
|
return {
|
|
@@ -554,11 +538,7 @@ export function getDomEditTargetKey(
|
|
|
554
538
|
}
|
|
555
539
|
|
|
556
540
|
export function isTextEditableSelection(selection: DomEditSelection): boolean {
|
|
557
|
-
return (
|
|
558
|
-
selection.textFields.length > 0 &&
|
|
559
|
-
!selection.isCompositionHost &&
|
|
560
|
-
!selection.isInsideLockedComposition
|
|
561
|
-
);
|
|
541
|
+
return resolveEditingSections(domEditSelectionToFacts(selection)).text;
|
|
562
542
|
}
|
|
563
543
|
|
|
564
544
|
// buildElementAgentPrompt is in domEditingAgentPrompt.ts
|
|
@@ -82,16 +82,6 @@ export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
82
82
|
true,
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
-
// Storyboard view: a top-level, toggleable view that renders STORYBOARD.md as a
|
|
86
|
-
// contact sheet of live HTML frame tiles, replacing the timeline/preview stage.
|
|
87
|
-
// Opt-in / off by default until the experience is ready for broad exposure.
|
|
88
|
-
// VITE_STUDIO_ENABLE_STORYBOARD=1 npx hyperframes preview
|
|
89
|
-
export const STUDIO_STORYBOARD_ENABLED = resolveStudioBooleanEnvFlag(
|
|
90
|
-
env,
|
|
91
|
-
["VITE_STUDIO_ENABLE_STORYBOARD", "VITE_STUDIO_STORYBOARD_ENABLED"],
|
|
92
|
-
false,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
85
|
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
96
86
|
|
|
97
87
|
// Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
|
|
@@ -50,10 +50,6 @@ interface RuntimeColorGradingStatus {
|
|
|
50
50
|
message: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function isColorGradingCapableElement(element: DomEditSelection): boolean {
|
|
54
|
-
return element.tagName === "video" || element.tagName === "img";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
53
|
function readColorGradingFromElement(element: DomEditSelection): NormalizedHfColorGrading {
|
|
58
54
|
const grading =
|
|
59
55
|
normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ??
|
|
@@ -10,12 +10,6 @@ import {
|
|
|
10
10
|
} from "./propertyPanelHelpers";
|
|
11
11
|
import { Section, SegmentedControl, SelectField, SliderControl } from "./propertyPanelPrimitives";
|
|
12
12
|
|
|
13
|
-
const MEDIA_TAGS = new Set(["video", "audio"]);
|
|
14
|
-
|
|
15
|
-
export function isMediaElement(element: DomEditSelection): boolean {
|
|
16
|
-
return MEDIA_TAGS.has(element.tagName);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
export function MediaSection({
|
|
20
14
|
projectDir,
|
|
21
15
|
element,
|
|
@@ -44,14 +44,11 @@ export interface ViewModeValue {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Owns the view-mode state
|
|
48
|
-
* mode is
|
|
49
|
-
* fully inert until the flag is on.
|
|
47
|
+
* Owns the view-mode state — initial read from `?view=`, toggling, popstate sync.
|
|
48
|
+
* Storyboard mode is always available; no flag gating.
|
|
50
49
|
*/
|
|
51
|
-
export function useViewModeState(
|
|
52
|
-
const [viewMode, setMode] = useState<StudioViewMode>(() =>
|
|
53
|
-
enabled ? readViewModeFromUrl() : "timeline",
|
|
54
|
-
);
|
|
50
|
+
export function useViewModeState(): ViewModeValue {
|
|
51
|
+
const [viewMode, setMode] = useState<StudioViewMode>(() => readViewModeFromUrl());
|
|
55
52
|
|
|
56
53
|
// Reflect genuine browser back/forward between history entries with a different
|
|
57
54
|
// `?view=`. Note: our own writes use `replaceState` (below), which does NOT fire
|
|
@@ -60,23 +57,17 @@ export function useViewModeState(enabled: boolean): ViewModeValue {
|
|
|
60
57
|
// the mount-time read); a scripted `pushState`/`replaceState` to `?view=` would not be
|
|
61
58
|
// reflected here, by design.
|
|
62
59
|
useEffect(() => {
|
|
63
|
-
if (!enabled) return;
|
|
64
60
|
const onPopState = () => setMode(readViewModeFromUrl());
|
|
65
61
|
window.addEventListener("popstate", onPopState);
|
|
66
62
|
return () => window.removeEventListener("popstate", onPopState);
|
|
67
|
-
}, [
|
|
63
|
+
}, []);
|
|
68
64
|
|
|
69
|
-
const setViewMode = useCallback(
|
|
70
|
-
(mode
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
writeViewModeToUrl(mode);
|
|
74
|
-
},
|
|
75
|
-
[enabled],
|
|
76
|
-
);
|
|
65
|
+
const setViewMode = useCallback((mode: StudioViewMode) => {
|
|
66
|
+
setMode(mode);
|
|
67
|
+
writeViewModeToUrl(mode);
|
|
68
|
+
}, []);
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
return useMemo(() => ({ viewMode: effectiveMode, setViewMode }), [effectiveMode, setViewMode]);
|
|
70
|
+
return useMemo(() => ({ viewMode, setViewMode }), [viewMode, setViewMode]);
|
|
80
71
|
}
|
|
81
72
|
|
|
82
73
|
const ViewModeContext = createContext<ViewModeValue | null>(null);
|
|
@@ -260,7 +260,9 @@ export function useDomEditSession({
|
|
|
260
260
|
onTrySdkPersist: sdkSession
|
|
261
261
|
? (selection, operations, originalContent, targetPath, options) => {
|
|
262
262
|
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
|
|
263
|
-
|
|
263
|
+
// Pass originalContent so the runtime-node filter can suppress hf-ids
|
|
264
|
+
// absent from source (script-created nodes the SDK can't model).
|
|
265
|
+
runResolverShadow(sdkSession, selection.hfId, operations, originalContent);
|
|
264
266
|
return sdkCutoverPersist(
|
|
265
267
|
selection,
|
|
266
268
|
operations,
|
|
@@ -226,6 +226,53 @@ describe("C. Resolver-parity detection", () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
it("C8 runtime-node filter: hfId absent from source → suppressed (not a resolver bug)", () => {
|
|
230
|
+
// The studio resolved a live-DOM element to an hf-id that the SDK session
|
|
231
|
+
// doesn't contain AND that never appears in the on-disk source — it's a
|
|
232
|
+
// node a composition <script> created at runtime (e.g. caption spans). Not
|
|
233
|
+
// a resolver divergence; suppress.
|
|
234
|
+
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
|
|
235
|
+
typeof sdkResolverShadowCheck
|
|
236
|
+
>[0];
|
|
237
|
+
const source = `<div data-hf-id="hf-static">no runtime id here</div>`;
|
|
238
|
+
const mismatches = sdkResolverShadowCheck(
|
|
239
|
+
session,
|
|
240
|
+
"hf-runtimeonly",
|
|
241
|
+
[{ type: "inline-style", property: "color", value: "red" }],
|
|
242
|
+
source,
|
|
243
|
+
);
|
|
244
|
+
expect(mismatches).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("C8 runtime-node filter: hfId PRESENT in source but missing from session → still flagged (real bug)", () => {
|
|
248
|
+
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
|
|
249
|
+
typeof sdkResolverShadowCheck
|
|
250
|
+
>[0];
|
|
251
|
+
const source = `<div data-hf-id="hf-realbug">in source, not in SDK session</div>`;
|
|
252
|
+
const mismatches = sdkResolverShadowCheck(
|
|
253
|
+
session,
|
|
254
|
+
"hf-realbug",
|
|
255
|
+
[{ type: "inline-style", property: "color", value: "red" }],
|
|
256
|
+
source,
|
|
257
|
+
);
|
|
258
|
+
expect(mismatches).toHaveLength(1);
|
|
259
|
+
expect(mismatches[0]?.kind).toBe("element_not_found");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("C8 sourceHfIdCount: emitted element_not_found carries source occurrence count", async () => {
|
|
263
|
+
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
|
|
264
|
+
const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
|
|
265
|
+
// id present twice in source (duplicate-id ambiguity) but absent from session
|
|
266
|
+
const source = `<div data-hf-id="hf-dup">a</div><div data-hf-id="hf-dup">b</div>`;
|
|
267
|
+
runResolverShadow(
|
|
268
|
+
session,
|
|
269
|
+
"hf-dup",
|
|
270
|
+
[{ type: "inline-style", property: "color", value: "red" }],
|
|
271
|
+
source,
|
|
272
|
+
);
|
|
273
|
+
expect(lastShadow()?.sourceHfIdCount).toBe(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
229
276
|
it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
|
|
230
277
|
const session = await openComposition(BASE_HTML);
|
|
231
278
|
// "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.
|
|
@@ -82,6 +82,14 @@ type AttrMap = Record<string, string | null>;
|
|
|
82
82
|
* Mirror resolveScoped here: exact scoped-path match, then canonical bare
|
|
83
83
|
* match, then first bare match — the resolvability dispatch actually has.
|
|
84
84
|
*/
|
|
85
|
+
// Count static `data-hf-id="<id>"` occurrences (both quote styles) in source.
|
|
86
|
+
// Substring split, not regex — no escaping, and the id never contains a quote.
|
|
87
|
+
function countHfIdInSource(source: string, id: string): number {
|
|
88
|
+
return (
|
|
89
|
+
source.split(`data-hf-id="${id}"`).length - 1 + (source.split(`data-hf-id='${id}'`).length - 1)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
function resolveSnapshot(session: Composition, id: string): FlatEl | null {
|
|
86
94
|
const els = session.getElements();
|
|
87
95
|
const exact = els.find((el) => el.scopedId === id);
|
|
@@ -164,8 +172,17 @@ export function sdkResolverShadowCheck(
|
|
|
164
172
|
session: Composition,
|
|
165
173
|
hfId: string,
|
|
166
174
|
ops: PatchOperation[],
|
|
175
|
+
sourceContent?: string,
|
|
167
176
|
): SdkResolverMismatch[] {
|
|
168
177
|
if (!resolveSnapshot(session, hfId)) {
|
|
178
|
+
// Runtime-node filter: an hf-id absent from the on-disk source the SDK
|
|
179
|
+
// parsed was never in the static DOM — it belongs to an element a
|
|
180
|
+
// composition <script> creates at runtime (e.g. caption word/group spans),
|
|
181
|
+
// which the SDK session cannot model by design. That is NOT a resolver bug,
|
|
182
|
+
// so suppress it. An hf-id PRESENT in source but missing from the session IS
|
|
183
|
+
// a genuine resolver divergence (the v0.6.110 class) — keep emitting that.
|
|
184
|
+
// ponytail: substring match; biases toward keeping signal on a loose hit.
|
|
185
|
+
if (sourceContent !== undefined && !sourceContent.includes(hfId)) return [];
|
|
169
186
|
return [{ kind: "element_not_found", hfId }];
|
|
170
187
|
}
|
|
171
188
|
|
|
@@ -241,17 +258,30 @@ export function runResolverShadow(
|
|
|
241
258
|
session: Composition,
|
|
242
259
|
hfId: string | null | undefined,
|
|
243
260
|
ops: PatchOperation[],
|
|
261
|
+
sourceContent?: string,
|
|
244
262
|
): void {
|
|
245
263
|
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
|
246
264
|
if (!hfId) return;
|
|
247
265
|
try {
|
|
248
|
-
const mismatches = sdkResolverShadowCheck(session, hfId, ops);
|
|
266
|
+
const mismatches = sdkResolverShadowCheck(session, hfId, ops, sourceContent);
|
|
249
267
|
// Emit only on divergence — parity is silent, matching recordResolverParity
|
|
250
268
|
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
|
|
251
269
|
// every style/text/attr edit (the editor's chattiest path) at default-ON.
|
|
252
270
|
if (mismatches.length === 0) return;
|
|
271
|
+
const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
|
|
253
272
|
trackStudioEvent("sdk_resolver_shadow", {
|
|
254
273
|
hfId,
|
|
274
|
+
// sessionElementCount > 0 + element_not_found = runtime-only element;
|
|
275
|
+
// sessionElementCount === 0 = session is empty/broken (actionable).
|
|
276
|
+
sessionElementCount: session.getElements().length,
|
|
277
|
+
// Count of data-hf-id="<id>" occurrences in source for an emitted
|
|
278
|
+
// element_not_found (the runtime-node filter already dropped absent-from-
|
|
279
|
+
// source ids, so an emitted one is in source ≥1×). >1 = duplicate ids →
|
|
280
|
+
// resolver picked the wrong instance; =1 = single static node the SDK
|
|
281
|
+
// parse dropped (foreign-content exclusion / sub-comp inlining gap).
|
|
282
|
+
...(isElementNotFound && sourceContent !== undefined
|
|
283
|
+
? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
|
|
284
|
+
: {}),
|
|
255
285
|
mismatchCount: mismatches.length,
|
|
256
286
|
mismatches: JSON.stringify(redactMismatches(mismatches)),
|
|
257
287
|
});
|
|
@@ -282,6 +312,7 @@ export function recordResolverParity(
|
|
|
282
312
|
trackStudioEvent("sdk_resolver_shadow", {
|
|
283
313
|
hfId,
|
|
284
314
|
opLabel,
|
|
315
|
+
sessionElementCount: session.getElements().length,
|
|
285
316
|
mismatchCount: 1,
|
|
286
317
|
mismatches: JSON.stringify([
|
|
287
318
|
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
|
|
@@ -310,11 +341,13 @@ export function recordAnimationResolverParity(
|
|
|
310
341
|
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
|
311
342
|
if (!session || !animationId) return;
|
|
312
343
|
try {
|
|
313
|
-
const
|
|
344
|
+
const elements = session.getElements();
|
|
345
|
+
const resolves = elements.some((el) => el.animationIds.includes(animationId));
|
|
314
346
|
if (resolves) return; // SDK locates the animation — parity
|
|
315
347
|
trackStudioEvent("sdk_resolver_shadow", {
|
|
316
348
|
animationId,
|
|
317
349
|
opLabel,
|
|
350
|
+
sessionElementCount: elements.length,
|
|
318
351
|
mismatchCount: 1,
|
|
319
352
|
mismatches: JSON.stringify([
|
|
320
353
|
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,
|