@hyperframes/studio 0.5.0-alpha.13 → 0.5.0-alpha.15
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/{hyperframes-player-vibA20NC.js → hyperframes-player-Cd8vYWxP.js} +2 -2
- package/dist/assets/index-DFLVGWTx.js +106 -0
- package/dist/assets/index-mXJ-UH9F.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +785 -377
- package/src/captions/generator.test.ts +19 -0
- package/src/captions/generator.ts +9 -2
- package/src/captions/hooks/useCaptionSync.ts +6 -1
- package/src/captions/parser.test.ts +14 -0
- package/src/captions/parser.ts +1 -0
- package/src/components/editor/DomEditOverlay.test.ts +241 -0
- package/src/components/editor/DomEditOverlay.tsx +970 -115
- package/src/components/editor/PropertyPanel.tsx +91 -83
- package/src/components/editor/domEditing.test.ts +161 -29
- package/src/components/editor/domEditing.ts +84 -113
- package/src/components/editor/manualEdits.test.ts +945 -0
- package/src/components/editor/manualEdits.ts +1397 -0
- package/src/components/editor/manualOffsetDrag.test.ts +140 -0
- package/src/components/editor/manualOffsetDrag.ts +307 -0
- package/src/components/renders/RenderQueue.tsx +10 -3
- package/src/hooks/usePersistentEditHistory.test.ts +1 -0
- package/src/hooks/usePersistentEditHistory.ts +3 -2
- package/src/player/components/CompositionThumbnail.test.ts +1 -1
- package/src/player/components/CompositionThumbnail.tsx +1 -1
- package/src/player/components/Player.tsx +54 -9
- package/src/player/hooks/useTimelinePlayer.test.ts +1 -0
- package/src/utils/clipboard.test.ts +1 -0
- package/src/utils/frameCapture.ts +3 -1
- package/src/utils/projectRouting.test.ts +87 -0
- package/src/utils/projectRouting.ts +27 -0
- package/dist/assets/index-JhhmFie-.js +0 -105
- package/dist/assets/index-KioPDrX6.css +0 -1
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type GradientModel,
|
|
39
39
|
} from "./gradientValue";
|
|
40
40
|
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
|
41
|
+
import { readStudioBoxSize, readStudioPathOffset } from "./manualEdits";
|
|
41
42
|
import {
|
|
42
43
|
COMMON_LOCAL_FONT_FAMILIES,
|
|
43
44
|
googleFontStylesheetUrl,
|
|
@@ -54,17 +55,17 @@ interface PropertyPanelProps {
|
|
|
54
55
|
copiedAgentPrompt: boolean;
|
|
55
56
|
onClearSelection: () => void;
|
|
56
57
|
onSetStyle: (prop: string, value: string) => void;
|
|
58
|
+
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
|
59
|
+
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
|
57
60
|
onSetText: (value: string, fieldKey?: string) => void;
|
|
58
61
|
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
59
62
|
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
|
60
63
|
onRemoveTextField: (fieldKey: string) => void;
|
|
61
|
-
|
|
64
|
+
onResetManualEdits: (element: DomEditSelection) => void;
|
|
62
65
|
onAskAgent: () => void;
|
|
63
|
-
onCopyAgentInstruction: (instruction: string) => void | Promise<void>;
|
|
64
66
|
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
65
67
|
fontAssets?: ImportedFontAsset[];
|
|
66
68
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
67
|
-
allowLayoutDetach?: boolean;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const FIELD =
|
|
@@ -117,17 +118,6 @@ interface FontOption {
|
|
|
117
118
|
|
|
118
119
|
const COLOR_PICKER_SIZE = { width: 292, height: 386 };
|
|
119
120
|
|
|
120
|
-
function buildMakeMovableAgentInstruction(reason: string): string {
|
|
121
|
-
return [
|
|
122
|
-
"Make this selected HyperFrames element movable in Studio.",
|
|
123
|
-
`Studio blocked direct move/resize because: ${reason}`,
|
|
124
|
-
"Refactor only this element safely.",
|
|
125
|
-
"Preserve its current visual position, timing, and sibling layout intent where possible.",
|
|
126
|
-
"If it is transform-driven, replace transform-based positioning with absolute pixel geometry.",
|
|
127
|
-
"If layout flow owns it, detach conservatively with position: absolute, px left/top/width/height, and margin: 0.",
|
|
128
|
-
].join(" ");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
function colorFromCss(value: string): ParsedColor {
|
|
132
122
|
return parseCssColor(value) ?? { red: 0, green: 0, blue: 0, alpha: 1 };
|
|
133
123
|
}
|
|
@@ -184,6 +174,17 @@ function parseNumericToken(value: string | undefined): ParsedNumericToken | null
|
|
|
184
174
|
};
|
|
185
175
|
}
|
|
186
176
|
|
|
177
|
+
function parsePxMetricValue(value: string): number | null {
|
|
178
|
+
const token = parseNumericToken(value);
|
|
179
|
+
if (!token) return null;
|
|
180
|
+
if (token.unit && token.unit.toLowerCase() !== "px") return null;
|
|
181
|
+
return token.value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatPxMetricValue(value: number): string {
|
|
185
|
+
return `${formatNumericValue(value)}px`;
|
|
186
|
+
}
|
|
187
|
+
|
|
187
188
|
function adjustNumericToken(
|
|
188
189
|
value: string,
|
|
189
190
|
direction: 1 | -1,
|
|
@@ -1975,17 +1976,17 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
1975
1976
|
copiedAgentPrompt,
|
|
1976
1977
|
onClearSelection,
|
|
1977
1978
|
onSetStyle,
|
|
1979
|
+
onSetManualOffset,
|
|
1980
|
+
onSetManualSize,
|
|
1978
1981
|
onSetText,
|
|
1979
1982
|
onSetTextFieldStyle,
|
|
1980
1983
|
onAddTextField,
|
|
1981
1984
|
onRemoveTextField,
|
|
1982
|
-
|
|
1985
|
+
onResetManualEdits,
|
|
1983
1986
|
onAskAgent,
|
|
1984
|
-
onCopyAgentInstruction,
|
|
1985
1987
|
onImportAssets,
|
|
1986
1988
|
fontAssets = [],
|
|
1987
1989
|
onImportFonts,
|
|
1988
|
-
allowLayoutDetach = true,
|
|
1989
1990
|
}: PropertyPanelProps) {
|
|
1990
1991
|
const styles = element?.computedStyles ?? EMPTY_STYLES;
|
|
1991
1992
|
const selectionColors = useMemo(() => collectSelectionColors(styles), [styles]);
|
|
@@ -2021,28 +2022,60 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2021
2022
|
<Eye size={18} className="mb-3 text-neutral-600" />
|
|
2022
2023
|
<p className="text-sm font-medium text-neutral-200">Select an element in the preview.</p>
|
|
2023
2024
|
<p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
|
|
2024
|
-
The inspector is tuned for
|
|
2025
|
-
|
|
2025
|
+
The inspector is tuned for element edits with safer geometry controls, color picking, and
|
|
2026
|
+
cleaner grouped layer controls.
|
|
2026
2027
|
</p>
|
|
2027
2028
|
</div>
|
|
2028
2029
|
);
|
|
2029
2030
|
}
|
|
2030
2031
|
|
|
2031
2032
|
const styleEditingDisabled = !element.capabilities.canEditStyles;
|
|
2032
|
-
const
|
|
2033
|
-
const
|
|
2033
|
+
const manualOffsetEditingDisabled = !element.capabilities.canApplyManualOffset;
|
|
2034
|
+
const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
|
|
2034
2035
|
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
|
|
2035
2036
|
const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0;
|
|
2036
2037
|
const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100);
|
|
2037
2038
|
const clipContent = ["hidden", "clip"].includes((styles.overflow ?? "").trim());
|
|
2038
2039
|
const sourceLabel = element.id ? `#${element.id}` : element.selector;
|
|
2039
2040
|
const showEditableSections = element.capabilities.canEditStyles;
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
?
|
|
2045
|
-
:
|
|
2041
|
+
const manualOffset = readStudioPathOffset(element.element);
|
|
2042
|
+
const manualSize = readStudioBoxSize(element.element);
|
|
2043
|
+
const resolvedWidth =
|
|
2044
|
+
manualSize.width > 0
|
|
2045
|
+
? manualSize.width
|
|
2046
|
+
: (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
|
|
2047
|
+
const resolvedHeight =
|
|
2048
|
+
manualSize.height > 0
|
|
2049
|
+
? manualSize.height
|
|
2050
|
+
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
|
2051
|
+
|
|
2052
|
+
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
|
2053
|
+
const parsed = parsePxMetricValue(nextValue);
|
|
2054
|
+
if (parsed == null) return;
|
|
2055
|
+
const current = readStudioPathOffset(element.element);
|
|
2056
|
+
onSetManualOffset(element, {
|
|
2057
|
+
x: axis === "x" ? parsed : current.x,
|
|
2058
|
+
y: axis === "y" ? parsed : current.y,
|
|
2059
|
+
});
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
const commitManualSize = (axis: "width" | "height", nextValue: string) => {
|
|
2063
|
+
const parsed = parsePxMetricValue(nextValue);
|
|
2064
|
+
if (parsed == null || parsed <= 0) return;
|
|
2065
|
+
const current = readStudioBoxSize(element.element);
|
|
2066
|
+
const width =
|
|
2067
|
+
current.width > 0
|
|
2068
|
+
? current.width
|
|
2069
|
+
: (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
|
|
2070
|
+
const height =
|
|
2071
|
+
current.height > 0
|
|
2072
|
+
? current.height
|
|
2073
|
+
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
|
2074
|
+
onSetManualSize(element, {
|
|
2075
|
+
width: axis === "width" ? parsed : width,
|
|
2076
|
+
height: axis === "height" ? parsed : height,
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2046
2079
|
|
|
2047
2080
|
const handleFillModeChange = (nextMode: string) => {
|
|
2048
2081
|
setPreferredFillMode(nextMode);
|
|
@@ -2078,14 +2111,25 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2078
2111
|
<X size={13} />
|
|
2079
2112
|
</button>
|
|
2080
2113
|
</div>
|
|
2081
|
-
<
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2114
|
+
<div className="mt-4 flex min-w-0 flex-wrap items-center gap-2">
|
|
2115
|
+
<button
|
|
2116
|
+
type="button"
|
|
2117
|
+
onClick={onAskAgent}
|
|
2118
|
+
className="inline-flex h-8 items-center justify-center gap-2 rounded-xl border border-neutral-700 bg-neutral-950 px-3.5 text-[11px] font-medium text-neutral-100 transition-colors hover:border-studio-accent/40 hover:text-studio-accent"
|
|
2119
|
+
>
|
|
2120
|
+
<MessageSquare size={15} />
|
|
2121
|
+
<span>{copiedAgentPrompt ? "Prompt copied" : "Ask agent"}</span>
|
|
2122
|
+
</button>
|
|
2123
|
+
<button
|
|
2124
|
+
type="button"
|
|
2125
|
+
onClick={() => onResetManualEdits(element)}
|
|
2126
|
+
title="Reset move, size, and rotation edits"
|
|
2127
|
+
className="inline-flex h-8 items-center justify-center gap-2 rounded-xl border border-neutral-700 bg-neutral-950 px-3.5 text-[11px] font-medium text-neutral-100 transition-colors hover:border-neutral-500 hover:text-white"
|
|
2128
|
+
>
|
|
2129
|
+
<RotateCcw size={14} />
|
|
2130
|
+
<span>Reset edits</span>
|
|
2131
|
+
</button>
|
|
2132
|
+
</div>
|
|
2089
2133
|
</div>
|
|
2090
2134
|
|
|
2091
2135
|
<div className="flex-1 overflow-y-auto">
|
|
@@ -2093,65 +2137,29 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
2093
2137
|
<div className={RESPONSIVE_GRID}>
|
|
2094
2138
|
<MetricField
|
|
2095
2139
|
label="X"
|
|
2096
|
-
value={
|
|
2097
|
-
disabled={
|
|
2098
|
-
onCommit={(next) =>
|
|
2140
|
+
value={formatPxMetricValue(manualOffset.x)}
|
|
2141
|
+
disabled={manualOffsetEditingDisabled}
|
|
2142
|
+
onCommit={(next) => commitManualOffset("x", next)}
|
|
2099
2143
|
/>
|
|
2100
2144
|
<MetricField
|
|
2101
2145
|
label="Y"
|
|
2102
|
-
value={
|
|
2103
|
-
disabled={
|
|
2104
|
-
onCommit={(next) =>
|
|
2146
|
+
value={formatPxMetricValue(manualOffset.y)}
|
|
2147
|
+
disabled={manualOffsetEditingDisabled}
|
|
2148
|
+
onCommit={(next) => commitManualOffset("y", next)}
|
|
2105
2149
|
/>
|
|
2106
2150
|
<MetricField
|
|
2107
2151
|
label="W"
|
|
2108
|
-
value={
|
|
2109
|
-
disabled={
|
|
2110
|
-
onCommit={(next) =>
|
|
2152
|
+
value={formatPxMetricValue(resolvedWidth)}
|
|
2153
|
+
disabled={manualSizeEditingDisabled}
|
|
2154
|
+
onCommit={(next) => commitManualSize("width", next)}
|
|
2111
2155
|
/>
|
|
2112
2156
|
<MetricField
|
|
2113
2157
|
label="H"
|
|
2114
|
-
value={
|
|
2115
|
-
disabled={
|
|
2116
|
-
onCommit={(next) =>
|
|
2158
|
+
value={formatPxMetricValue(resolvedHeight)}
|
|
2159
|
+
disabled={manualSizeEditingDisabled}
|
|
2160
|
+
onCommit={(next) => commitManualSize("height", next)}
|
|
2117
2161
|
/>
|
|
2118
2162
|
</div>
|
|
2119
|
-
{disabledMoveReason && (
|
|
2120
|
-
<div className="mt-4 flex min-w-0 flex-wrap items-center justify-between gap-3 border-l border-amber-500/40 pl-3">
|
|
2121
|
-
<div className="min-w-0 text-[11px] leading-5 text-neutral-400">
|
|
2122
|
-
<div className="font-medium text-amber-100">{disabledMoveReason}</div>
|
|
2123
|
-
<div>Copy a targeted prompt so an agent can refactor this layer safely.</div>
|
|
2124
|
-
</div>
|
|
2125
|
-
<button
|
|
2126
|
-
type="button"
|
|
2127
|
-
onClick={() => {
|
|
2128
|
-
void Promise.resolve(
|
|
2129
|
-
onCopyAgentInstruction(buildMakeMovableAgentInstruction(disabledMoveReason)),
|
|
2130
|
-
);
|
|
2131
|
-
}}
|
|
2132
|
-
className="inline-flex h-8 flex-shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-3 text-[11px] font-medium text-neutral-100 transition-colors hover:border-amber-400/70 hover:text-amber-100"
|
|
2133
|
-
>
|
|
2134
|
-
{copiedAgentPrompt ? "Prompt copied" : "Ask agent to make movable"}
|
|
2135
|
-
</button>
|
|
2136
|
-
</div>
|
|
2137
|
-
)}
|
|
2138
|
-
{allowLayoutDetach && element.capabilities.canDetachFromLayout && (
|
|
2139
|
-
<div className="mt-4 flex min-w-0 flex-wrap items-center justify-between gap-3 border-l border-amber-500/40 pl-3">
|
|
2140
|
-
<div className="min-w-0 text-[11px] leading-5 text-neutral-400">
|
|
2141
|
-
<div className="font-medium text-neutral-200">
|
|
2142
|
-
This layer is controlled by layout.
|
|
2143
|
-
</div>
|
|
2144
|
-
<div>Detaches from flex/grid flow and preserves current visual position.</div>
|
|
2145
|
-
</div>
|
|
2146
|
-
<button
|
|
2147
|
-
type="button"
|
|
2148
|
-
onClick={onDetachFromLayout}
|
|
2149
|
-
className="inline-flex h-8 flex-shrink-0 items-center rounded-lg border border-neutral-700 bg-neutral-950 px-3 text-[11px] font-medium text-neutral-100 transition-colors hover:border-amber-400/70 hover:text-amber-100"
|
|
2150
|
-
>
|
|
2151
|
-
Make movable
|
|
2152
|
-
</button>
|
|
2153
|
-
</div>
|
|
2154
|
-
)}
|
|
2155
2163
|
</Section>
|
|
2156
2164
|
|
|
2157
2165
|
{showEditableSections && isFlex && (
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { Window } from "happy-dom";
|
|
3
3
|
import {
|
|
4
|
-
buildDomEditMovePatchOperations,
|
|
5
|
-
buildDomEditResizePatchOperations,
|
|
6
4
|
buildDomEditStylePatchOperation,
|
|
7
5
|
buildElementAgentPrompt,
|
|
8
6
|
findElementForSelection,
|
|
7
|
+
getDomEditNonEditableReason,
|
|
8
|
+
getDomEditTargetKey,
|
|
9
9
|
isTextEditableSelection,
|
|
10
10
|
serializeDomEditTextFields,
|
|
11
11
|
type DomEditSelection,
|
|
@@ -47,7 +47,9 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
47
47
|
canEditStyles: true,
|
|
48
48
|
canMove: true,
|
|
49
49
|
canResize: true,
|
|
50
|
-
|
|
50
|
+
canApplyManualOffset: true,
|
|
51
|
+
canApplyManualSize: true,
|
|
52
|
+
canApplyManualRotation: true,
|
|
51
53
|
reasonIfDisabled: undefined,
|
|
52
54
|
});
|
|
53
55
|
});
|
|
@@ -75,8 +77,10 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
75
77
|
canEditStyles: true,
|
|
76
78
|
canMove: false,
|
|
77
79
|
canResize: false,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
canApplyManualOffset: true,
|
|
81
|
+
canApplyManualSize: true,
|
|
82
|
+
canApplyManualRotation: true,
|
|
83
|
+
reasonIfDisabled: undefined,
|
|
80
84
|
});
|
|
81
85
|
});
|
|
82
86
|
|
|
@@ -104,7 +108,9 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
104
108
|
).toMatchObject({
|
|
105
109
|
canMove: false,
|
|
106
110
|
canResize: false,
|
|
107
|
-
|
|
111
|
+
canApplyManualOffset: true,
|
|
112
|
+
canApplyManualSize: true,
|
|
113
|
+
canApplyManualRotation: true,
|
|
108
114
|
});
|
|
109
115
|
});
|
|
110
116
|
|
|
@@ -132,7 +138,7 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
132
138
|
).toMatchObject({
|
|
133
139
|
canMove: true,
|
|
134
140
|
canResize: true,
|
|
135
|
-
|
|
141
|
+
canApplyManualOffset: true,
|
|
136
142
|
});
|
|
137
143
|
});
|
|
138
144
|
|
|
@@ -191,7 +197,7 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
191
197
|
});
|
|
192
198
|
|
|
193
199
|
describe("resolveDomEditSelection", () => {
|
|
194
|
-
it("
|
|
200
|
+
it("keeps composition host transforms disabled in master view", () => {
|
|
195
201
|
expect(
|
|
196
202
|
resolveDomEditCapabilities({
|
|
197
203
|
selector: "#detail-host",
|
|
@@ -217,15 +223,49 @@ describe("resolveDomEditSelection", () => {
|
|
|
217
223
|
canEditStyles: false,
|
|
218
224
|
canMove: true,
|
|
219
225
|
canResize: true,
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
canApplyManualOffset: false,
|
|
227
|
+
canApplyManualSize: false,
|
|
228
|
+
canApplyManualRotation: false,
|
|
229
|
+
reasonIfDisabled: "Select an internal layer to transform it.",
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("resolves child clicks inside a composition host to the child in master view", () => {
|
|
234
|
+
const document = createDocument(`
|
|
235
|
+
<div data-composition-id="main">
|
|
236
|
+
<div
|
|
237
|
+
id="detail-host"
|
|
238
|
+
class="clip"
|
|
239
|
+
data-composition-id="detail-card"
|
|
240
|
+
data-composition-file="compositions/detail-card.html"
|
|
241
|
+
>
|
|
242
|
+
<span id="inner-copy">Nested scene</span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
`);
|
|
246
|
+
|
|
247
|
+
const child = document.getElementById("inner-copy") as HTMLElement;
|
|
248
|
+
const selection = resolveDomEditSelection(child, {
|
|
249
|
+
activeCompositionPath: null,
|
|
250
|
+
isMasterView: true,
|
|
222
251
|
});
|
|
252
|
+
|
|
253
|
+
expect(selection?.id).toBe("inner-copy");
|
|
254
|
+
expect(selection?.sourceFile).toBe("compositions/detail-card.html");
|
|
255
|
+
expect(selection?.isCompositionHost).toBe(false);
|
|
256
|
+
expect(selection?.capabilities.canApplyManualOffset).toBe(true);
|
|
257
|
+
expect(selection?.capabilities.canEditStyles).toBe(true);
|
|
223
258
|
});
|
|
224
259
|
|
|
225
|
-
it("
|
|
260
|
+
it("does not prefer a scene host clip ancestor when selecting inside it", () => {
|
|
226
261
|
const document = createDocument(`
|
|
227
262
|
<div data-composition-id="main">
|
|
228
|
-
<div
|
|
263
|
+
<div
|
|
264
|
+
id="detail-host"
|
|
265
|
+
class="clip"
|
|
266
|
+
data-composition-id="detail-card"
|
|
267
|
+
data-composition-file="compositions/detail-card.html"
|
|
268
|
+
>
|
|
229
269
|
<span id="inner-copy">Nested scene</span>
|
|
230
270
|
</div>
|
|
231
271
|
</div>
|
|
@@ -235,12 +275,40 @@ describe("resolveDomEditSelection", () => {
|
|
|
235
275
|
const selection = resolveDomEditSelection(child, {
|
|
236
276
|
activeCompositionPath: null,
|
|
237
277
|
isMasterView: true,
|
|
278
|
+
preferClipAncestor: true,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(selection?.id).toBe("inner-copy");
|
|
282
|
+
expect(selection?.sourceFile).toBe("compositions/detail-card.html");
|
|
283
|
+
expect(selection?.isCompositionHost).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("still prefers an internal clip ancestor inside a scene", () => {
|
|
287
|
+
const document = createDocument(`
|
|
288
|
+
<div data-composition-id="main">
|
|
289
|
+
<div
|
|
290
|
+
id="detail-host"
|
|
291
|
+
class="clip"
|
|
292
|
+
data-composition-id="detail-card"
|
|
293
|
+
data-composition-file="compositions/detail-card.html"
|
|
294
|
+
>
|
|
295
|
+
<section id="nested-card" class="clip">
|
|
296
|
+
<span id="inner-copy">Nested scene</span>
|
|
297
|
+
</section>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
`);
|
|
301
|
+
|
|
302
|
+
const child = document.getElementById("inner-copy") as HTMLElement;
|
|
303
|
+
const selection = resolveDomEditSelection(child, {
|
|
304
|
+
activeCompositionPath: null,
|
|
305
|
+
isMasterView: true,
|
|
306
|
+
preferClipAncestor: true,
|
|
238
307
|
});
|
|
239
308
|
|
|
240
|
-
expect(selection?.id).toBe("
|
|
241
|
-
expect(selection?.
|
|
242
|
-
expect(selection?.
|
|
243
|
-
expect(selection?.capabilities.canEditStyles).toBe(false);
|
|
309
|
+
expect(selection?.id).toBe("nested-card");
|
|
310
|
+
expect(selection?.sourceFile).toBe("compositions/detail-card.html");
|
|
311
|
+
expect(selection?.isCompositionHost).toBe(false);
|
|
244
312
|
});
|
|
245
313
|
|
|
246
314
|
it("scopes class selector indexing to the same source file", () => {
|
|
@@ -265,6 +333,28 @@ describe("resolveDomEditSelection", () => {
|
|
|
265
333
|
expect(findElementForSelection(document, selection!, null)).toBe(rootChip);
|
|
266
334
|
});
|
|
267
335
|
|
|
336
|
+
it("resolves nested duplicate ids from master view without treating root as the nested source", () => {
|
|
337
|
+
const document = createDocument(`
|
|
338
|
+
<div data-composition-id="main">
|
|
339
|
+
<div id="card">Root card</div>
|
|
340
|
+
<div data-composition-id="nested" data-composition-file="scenes/nested.html">
|
|
341
|
+
<div id="card">Nested card</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
`);
|
|
345
|
+
|
|
346
|
+
const nestedCard = document.querySelector(
|
|
347
|
+
'[data-composition-file="scenes/nested.html"] #card',
|
|
348
|
+
) as HTMLElement;
|
|
349
|
+
const selection = resolveDomEditSelection(nestedCard, {
|
|
350
|
+
activeCompositionPath: null,
|
|
351
|
+
isMasterView: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(selection?.sourceFile).toBe("scenes/nested.html");
|
|
355
|
+
expect(findElementForSelection(document, selection!, null)).toBe(nestedCard);
|
|
356
|
+
});
|
|
357
|
+
|
|
268
358
|
it("prefers the nearest clip ancestor on single-click style selection", () => {
|
|
269
359
|
const document = createDocument(`
|
|
270
360
|
<section id="card" class="clip" style="left: 10px; top: 20px; width: 200px; height: 100px; position: absolute;">
|
|
@@ -373,23 +463,60 @@ describe("resolveDomEditSelection", () => {
|
|
|
373
463
|
expect(selection?.textFields.map((field) => field.tagName)).toEqual(["strong", "span"]);
|
|
374
464
|
expect(selection?.textFields.map((field) => field.value)).toEqual(["", ""]);
|
|
375
465
|
});
|
|
376
|
-
});
|
|
377
466
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
467
|
+
it("explains anonymous child elements that resolve to an editable parent", () => {
|
|
468
|
+
const document = createDocument(`
|
|
469
|
+
<div data-composition-id="main">
|
|
470
|
+
<div id="card">
|
|
471
|
+
<strong>Headline</strong>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
`);
|
|
475
|
+
|
|
476
|
+
const child = document.querySelector("strong") as HTMLElement;
|
|
477
|
+
const selection = resolveDomEditSelection(child, {
|
|
478
|
+
activeCompositionPath: null,
|
|
479
|
+
isMasterView: false,
|
|
480
|
+
preferClipAncestor: false,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(selection?.id).toBe("card");
|
|
484
|
+
expect(getDomEditNonEditableReason(child, selection)).toBe("Selection resolves to Card");
|
|
384
485
|
});
|
|
385
486
|
|
|
386
|
-
it("
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
487
|
+
it("does not mark an element as non-editable when Studio can edit it directly", () => {
|
|
488
|
+
const document = createDocument(`
|
|
489
|
+
<div data-composition-id="main">
|
|
490
|
+
<div id="card">Editable</div>
|
|
491
|
+
</div>
|
|
492
|
+
`);
|
|
493
|
+
|
|
494
|
+
const element = document.getElementById("card") as HTMLElement;
|
|
495
|
+
const selection = resolveDomEditSelection(element, {
|
|
496
|
+
activeCompositionPath: null,
|
|
497
|
+
isMasterView: false,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(getDomEditNonEditableReason(element, selection)).toBeNull();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("keeps duplicate class targets distinct for history keys", () => {
|
|
504
|
+
const first = getDomEditTargetKey({
|
|
505
|
+
sourceFile: "index.html",
|
|
506
|
+
selector: ".card",
|
|
507
|
+
selectorIndex: 0,
|
|
508
|
+
});
|
|
509
|
+
const second = getDomEditTargetKey({
|
|
510
|
+
sourceFile: "index.html",
|
|
511
|
+
selector: ".card",
|
|
512
|
+
selectorIndex: 1,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(first).not.toBe(second);
|
|
391
516
|
});
|
|
517
|
+
});
|
|
392
518
|
|
|
519
|
+
describe("patch builders and prompt builder", () => {
|
|
393
520
|
it("builds style patch operations", () => {
|
|
394
521
|
expect(buildDomEditStylePatchOperation("background-color", "rgb(15, 23, 42)")).toEqual({
|
|
395
522
|
type: "inline-style",
|
|
@@ -444,6 +571,9 @@ describe("patch builders and prompt builder", () => {
|
|
|
444
571
|
canEditStyles: true,
|
|
445
572
|
canMove: true,
|
|
446
573
|
canResize: true,
|
|
574
|
+
canApplyManualOffset: true,
|
|
575
|
+
canApplyManualSize: true,
|
|
576
|
+
canApplyManualRotation: true,
|
|
447
577
|
},
|
|
448
578
|
} satisfies DomEditSelection;
|
|
449
579
|
|
|
@@ -490,7 +620,9 @@ describe("patch builders and prompt builder", () => {
|
|
|
490
620
|
canEditStyles: true,
|
|
491
621
|
canMove: true,
|
|
492
622
|
canResize: true,
|
|
493
|
-
|
|
623
|
+
canApplyManualOffset: true,
|
|
624
|
+
canApplyManualSize: true,
|
|
625
|
+
canApplyManualRotation: true,
|
|
494
626
|
},
|
|
495
627
|
} satisfies DomEditSelection;
|
|
496
628
|
|