@hyperframes/studio 0.7.60 → 0.7.61
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-Xvx2hkrc.js +459 -0
- package/dist/assets/{index-cH6NfVV_.js → index-2mxh_HSy.js} +177 -177
- package/dist/assets/{index-D6etaey-.js → index-BCpoiv9S.js} +1 -1
- package/dist/assets/index-BhWig0mx.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-D-uFclFj.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1202 -673
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -8
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +44 -42
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
- package/src/components/editor/propertyPanelFlatTextSection.tsx +2 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
- package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
- package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
- package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
- package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
- package/src/components/storyboard/storyboardReviewStage.ts +45 -0
- package/src/components/storyboard/useFrameComments.ts +22 -6
- package/src/contexts/ViewModeContext.tsx +60 -7
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/player/lib/playbackTypes.ts +7 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.ts +8 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
|
@@ -388,48 +388,50 @@ export function PropertyPanelFlat({
|
|
|
388
388
|
),
|
|
389
389
|
});
|
|
390
390
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
391
|
+
if (sections.layout) {
|
|
392
|
+
groups.push({
|
|
393
|
+
id: "layout",
|
|
394
|
+
title: "Layout",
|
|
395
|
+
// No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
|
|
396
|
+
// (wheel/arrow keys only) — advertising "drag values to scrub" here lies.
|
|
397
|
+
summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
|
|
398
|
+
content: (
|
|
399
|
+
<FlatLayoutSection
|
|
400
|
+
element={element}
|
|
401
|
+
styles={styles}
|
|
402
|
+
onSetStyle={onSetStyle}
|
|
403
|
+
disabled={!element.capabilities.canEditStyles}
|
|
404
|
+
displayX={displayX}
|
|
405
|
+
displayY={displayY}
|
|
406
|
+
displayW={displayW}
|
|
407
|
+
displayH={displayH}
|
|
408
|
+
displayR={displayR}
|
|
409
|
+
manualOffsetEditingDisabled={manualOffsetEditingDisabled}
|
|
410
|
+
manualSizeEditingDisabled={manualSizeEditingDisabled}
|
|
411
|
+
manualRotationEditingDisabled={manualRotationEditingDisabled}
|
|
412
|
+
commitManualOffset={commitManualOffset}
|
|
413
|
+
commitManualSize={commitManualSize}
|
|
414
|
+
commitManualRotation={commitManualRotation}
|
|
415
|
+
gsapAnimId={gsapAnimId}
|
|
416
|
+
navKeyframes={navKeyframes}
|
|
417
|
+
currentPct={currentPct}
|
|
418
|
+
seekFromKfPct={seekFromKfPct}
|
|
419
|
+
animIdForProp={animIdForProp}
|
|
420
|
+
resolveAnimIdForProp={animIdForProp}
|
|
421
|
+
gsapRuntimeValues={gsapRuntimeValues}
|
|
422
|
+
gsapKeyframes={navKeyframes}
|
|
423
|
+
elStart={elStart}
|
|
424
|
+
elDuration={elDuration}
|
|
425
|
+
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
426
|
+
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
427
|
+
onSeekToTime={onSeekToTime}
|
|
428
|
+
onRemoveKeyframe={onRemoveKeyframe}
|
|
429
|
+
onConvertToKeyframes={onConvertToKeyframes}
|
|
430
|
+
onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
|
|
431
|
+
/>
|
|
432
|
+
),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
433
435
|
if (showMotionGroup) {
|
|
434
436
|
groups.push({
|
|
435
437
|
id: "motion",
|
|
@@ -106,13 +106,13 @@ describe("manual editing availability", () => {
|
|
|
106
106
|
expect(resolveStudioBooleanEnvFlag({ UNKNOWN: "maybe" }, ["UNKNOWN"], false)).toBe(false);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it("defaults the flat inspector flag to
|
|
110
|
-
const
|
|
111
|
-
expect(
|
|
109
|
+
it("defaults the flat inspector flag to true and honors an explicit override", async () => {
|
|
110
|
+
const on = await loadAvailabilityWithEnv({});
|
|
111
|
+
expect(on.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(true);
|
|
112
112
|
|
|
113
|
-
const
|
|
114
|
-
VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "
|
|
113
|
+
const off = await loadAvailabilityWithEnv({
|
|
114
|
+
VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "false",
|
|
115
115
|
});
|
|
116
|
-
expect(
|
|
116
|
+
expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
@@ -98,12 +98,14 @@ export const STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
98
98
|
);
|
|
99
99
|
|
|
100
100
|
// Studio inspector redesign ("Ledger, flat" — design_handoff_studio_inspector):
|
|
101
|
-
// flat identity header/footer/groups
|
|
102
|
-
//
|
|
101
|
+
// flat identity header/footer/groups. Default true as of v0.7.59+ bug-fix pass
|
|
102
|
+
// (right-aligned values, Stroke select-only, promote-badge overlap, Layout/
|
|
103
|
+
// Style section gating); disable via VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false
|
|
104
|
+
// to fall back to the legacy panel.
|
|
103
105
|
export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
|
|
104
106
|
env,
|
|
105
107
|
["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
|
|
106
|
-
|
|
108
|
+
true,
|
|
107
109
|
);
|
|
108
110
|
|
|
109
111
|
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|
|
@@ -38,6 +38,16 @@ describe("FlatRow", () => {
|
|
|
38
38
|
act(() => root.unmount());
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it("right-aligns the value input — the flat inspector's justify-between row layout leaves a left-aligned value looking stranded at the edge of its own box", () => {
|
|
42
|
+
const { host, root } = renderInto(
|
|
43
|
+
<FlatRow label="Size" value="72px" tier="explicitCustom" onCommit={vi.fn()} />,
|
|
44
|
+
);
|
|
45
|
+
const input = host.querySelector("input");
|
|
46
|
+
expect(input?.className).toContain("text-right");
|
|
47
|
+
expect(input?.className).not.toContain("text-left");
|
|
48
|
+
act(() => root.unmount());
|
|
49
|
+
});
|
|
50
|
+
|
|
41
51
|
it("renders the explicitCustom tier with a mint value and a reset button", () => {
|
|
42
52
|
const onReset = vi.fn();
|
|
43
53
|
const { host, root } = renderInto(
|
|
@@ -12,16 +12,3 @@ export const STROKE_STYLE_OPTIONS: string[] = [
|
|
|
12
12
|
"inset",
|
|
13
13
|
"outset",
|
|
14
14
|
];
|
|
15
|
-
|
|
16
|
-
export function formatStrokeSummary(widthPx: number, style: string): string {
|
|
17
|
-
return `${widthPx}px ${style}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function parseStrokeSummary(text: string): { widthPx: number; style: string } | null {
|
|
21
|
-
const match = /^\s*(-?\d+(?:\.\d+)?)px\s+(\S+)\s*$/.exec(text);
|
|
22
|
-
if (!match) return null;
|
|
23
|
-
const widthPx = Number.parseFloat(match[1]);
|
|
24
|
-
const style = match[2];
|
|
25
|
-
if (!Number.isFinite(widthPx) || !style) return null;
|
|
26
|
-
return { widthPx, style };
|
|
27
|
-
}
|
|
@@ -175,31 +175,43 @@ function setInputValue(input: HTMLInputElement, nextValue: string) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
describe("FlatStyleSection — Stroke and Radius", () => {
|
|
178
|
-
it("renders
|
|
178
|
+
it("renders a width-only Stroke width row — style is only ever set through the Stroke style select below it", () => {
|
|
179
179
|
const { host, root } = renderSection(STROKE_STYLES);
|
|
180
|
-
expect(host.textContent).toContain("Stroke");
|
|
181
|
-
expect(
|
|
180
|
+
expect(host.textContent).toContain("Stroke width");
|
|
181
|
+
expect(host.textContent).toContain("Stroke style");
|
|
182
|
+
expect(getFlatRowInput(host, "Stroke width").value).toBe("1px");
|
|
182
183
|
act(() => root.unmount());
|
|
183
184
|
});
|
|
184
185
|
|
|
185
|
-
it("commits
|
|
186
|
+
it("commits a new stroke width without touching the existing style", async () => {
|
|
186
187
|
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
|
187
|
-
await commitFlatRowInput(host, "Stroke", "2px
|
|
188
|
+
await commitFlatRowInput(host, "Stroke width", "2px");
|
|
188
189
|
expect(onSetStyle).toHaveBeenCalledWith("border-width", "2px");
|
|
189
|
-
expect(onSetStyle).toHaveBeenCalledWith("border-style",
|
|
190
|
+
expect(onSetStyle).not.toHaveBeenCalledWith("border-style", expect.anything());
|
|
191
|
+
act(() => root.unmount());
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("defaults a from-zero width commit to solid, since a width with no style renders no visible border", async () => {
|
|
195
|
+
const { host, root, onSetStyle } = renderSection({
|
|
196
|
+
"border-width": "0px",
|
|
197
|
+
"border-style": "none",
|
|
198
|
+
"border-color": "rgba(255,255,255,.12)",
|
|
199
|
+
});
|
|
200
|
+
await commitFlatRowInput(host, "Stroke width", "3px");
|
|
201
|
+
expect(onSetStyle).toHaveBeenCalledWith("border-style", "solid");
|
|
190
202
|
act(() => root.unmount());
|
|
191
203
|
});
|
|
192
204
|
|
|
193
205
|
it("clamps an out-of-range stroke width commit to 200px (fix 2)", async () => {
|
|
194
206
|
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
|
195
|
-
await commitFlatRowInput(host, "Stroke", "9999px
|
|
207
|
+
await commitFlatRowInput(host, "Stroke width", "9999px");
|
|
196
208
|
expect(onSetStyle).toHaveBeenCalledWith("border-width", "200px");
|
|
197
209
|
act(() => root.unmount());
|
|
198
210
|
});
|
|
199
211
|
|
|
200
|
-
it("
|
|
212
|
+
it("ignores an unparseable stroke width commit", async () => {
|
|
201
213
|
const { host, root, onSetStyle } = renderSection(STROKE_STYLES);
|
|
202
|
-
await commitFlatRowInput(host, "Stroke", "
|
|
214
|
+
await commitFlatRowInput(host, "Stroke width", "bogus");
|
|
203
215
|
expect(onSetStyle).not.toHaveBeenCalled();
|
|
204
216
|
act(() => root.unmount());
|
|
205
217
|
});
|
|
@@ -3,11 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
|
4
4
|
import { buildDefaultGradientModel, serializeGradient } from "./gradientValue";
|
|
5
5
|
import { BorderRadiusEditor } from "./BorderRadiusEditor";
|
|
6
|
-
import {
|
|
7
|
-
formatStrokeSummary,
|
|
8
|
-
parseStrokeSummary,
|
|
9
|
-
STROKE_STYLE_OPTIONS,
|
|
10
|
-
} from "./propertyPanelFlatStyleHelpers";
|
|
6
|
+
import { STROKE_STYLE_OPTIONS } from "./propertyPanelFlatStyleHelpers";
|
|
11
7
|
import {
|
|
12
8
|
buildBoxShadowPresetValue,
|
|
13
9
|
buildClipPathValue,
|
|
@@ -147,10 +143,9 @@ function FlatFillFields({
|
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
/* ------------------------------------------------------------------ */
|
|
150
|
-
/* Flat Stroke row —
|
|
146
|
+
/* Flat Stroke row — width (numeric), style (select), color */
|
|
151
147
|
/* ------------------------------------------------------------------ */
|
|
152
148
|
|
|
153
|
-
// fallow-ignore-next-line complexity
|
|
154
149
|
function FlatStrokeRow({
|
|
155
150
|
styles,
|
|
156
151
|
disabled,
|
|
@@ -167,52 +162,40 @@ function FlatStrokeRow({
|
|
|
167
162
|
const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none";
|
|
168
163
|
const borderColorValue =
|
|
169
164
|
styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)";
|
|
170
|
-
const
|
|
171
|
-
const tier = resolveValueTier(
|
|
172
|
-
styles["border-width"] != null || styles["border-style"] != null ? summary : undefined,
|
|
173
|
-
formatStrokeSummary(0, "none"),
|
|
174
|
-
);
|
|
165
|
+
const widthDisplay = formatPxMetricValue(borderWidthValue);
|
|
175
166
|
|
|
176
167
|
return (
|
|
177
168
|
<>
|
|
178
169
|
<FlatRow
|
|
179
|
-
label="Stroke"
|
|
180
|
-
value={
|
|
181
|
-
tier={
|
|
170
|
+
label="Stroke width"
|
|
171
|
+
value={widthDisplay}
|
|
172
|
+
tier={resolveValueTier(styles["border-width"], "0px")}
|
|
182
173
|
disabled={disabled}
|
|
183
174
|
onCommit={async (next) => {
|
|
184
|
-
const
|
|
185
|
-
if (!parsed) return;
|
|
186
|
-
if (!STROKE_STYLE_OPTIONS.includes(parsed.style)) return;
|
|
187
|
-
const normalizedWidth = normalizePanelPxValue(`${parsed.widthPx}px`, {
|
|
175
|
+
const normalizedWidth = normalizePanelPxValue(next, {
|
|
188
176
|
min: 0,
|
|
189
177
|
max: 200,
|
|
190
178
|
fallback: borderWidthValue,
|
|
191
179
|
});
|
|
192
180
|
if (!normalizedWidth) return;
|
|
181
|
+
// buildStrokeWidthStyleUpdates already covers "a width typed in
|
|
182
|
+
// from 0 needs a style to actually render a visible border" —
|
|
183
|
+
// it only defaults to solid when the current style is none/hidden,
|
|
184
|
+
// never clobbering an already-chosen style (dashed, dotted, …).
|
|
193
185
|
for (const [property, value] of buildStrokeWidthStyleUpdates(
|
|
194
186
|
normalizedWidth,
|
|
195
|
-
|
|
187
|
+
borderStyleValue,
|
|
196
188
|
)) {
|
|
197
189
|
await onSetStyle(property, value);
|
|
198
190
|
}
|
|
199
|
-
for (const [property, value] of buildStrokeStyleUpdates(parsed.style, normalizedWidth)) {
|
|
200
|
-
await onSetStyle(property, value);
|
|
201
|
-
}
|
|
202
191
|
}}
|
|
203
|
-
suffix={
|
|
204
|
-
<>
|
|
205
|
-
<span
|
|
206
|
-
className="h-4 w-4 flex-shrink-0 rounded border border-panel-border-input"
|
|
207
|
-
style={{ backgroundColor: borderColorValue }}
|
|
208
|
-
/>
|
|
209
|
-
<span className="font-mono text-[10px] text-panel-text-3">{borderColorValue}</span>
|
|
210
|
-
</>
|
|
211
|
-
}
|
|
212
192
|
/>
|
|
213
193
|
<FlatSelectRow
|
|
214
194
|
label="Stroke style"
|
|
215
195
|
value={borderStyleValue}
|
|
196
|
+
// Valid border-style keywords — the ONLY way to set this, since a
|
|
197
|
+
// free-text field here would require typing an exact CSS keyword
|
|
198
|
+
// (e.g. "dashed") with no indication of which ones are valid.
|
|
216
199
|
options={STROKE_STYLE_OPTIONS}
|
|
217
200
|
tier={resolveValueTier(styles["border-style"], "none")}
|
|
218
201
|
disabled={disabled}
|
|
@@ -267,7 +267,7 @@ export function FlatTextSection({
|
|
|
267
267
|
|
|
268
268
|
if (textFields.length > 1) {
|
|
269
269
|
return (
|
|
270
|
-
<div className="space-y-
|
|
270
|
+
<div className="space-y-2.5">
|
|
271
271
|
<FlatTextLayerList
|
|
272
272
|
fields={textFields}
|
|
273
273
|
activeFieldKey={activeField.key}
|
|
@@ -295,7 +295,7 @@ export function FlatTextSection({
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
return (
|
|
298
|
-
<div className="space-y-
|
|
298
|
+
<div className="space-y-2.5">
|
|
299
299
|
<FlatTextFieldEditor
|
|
300
300
|
field={activeField}
|
|
301
301
|
styles={styles}
|
|
@@ -134,6 +134,13 @@ describe("classic property-panel primitive telemetry", () => {
|
|
|
134
134
|
const input = host.querySelector("input");
|
|
135
135
|
if (!input) throw new Error("expected metric input");
|
|
136
136
|
|
|
137
|
+
// Regression guard for CommitField's shared `align` default: the classic
|
|
138
|
+
// panel lays out label-then-value inline, where left-aligned reads
|
|
139
|
+
// naturally — this must stay left even though the flat inspector's
|
|
140
|
+
// FlatRow now opts into `align="right"` for its own justify-between rows.
|
|
141
|
+
expect(input.className).toContain("text-left");
|
|
142
|
+
expect(input.className).not.toContain("text-right");
|
|
143
|
+
|
|
137
144
|
act(() => blurInput(input));
|
|
138
145
|
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
139
146
|
|
|
@@ -448,6 +455,13 @@ function representativeElement() {
|
|
|
448
455
|
|
|
449
456
|
describe("classic PropertyPanel input coverage", () => {
|
|
450
457
|
it("emits only named, known-section events across body inputs and header/footer chrome", async () => {
|
|
458
|
+
vi.resetModules();
|
|
459
|
+
vi.doMock("./manualEditingAvailability", async () => {
|
|
460
|
+
const actual = await vi.importActual<typeof import("./manualEditingAvailability")>(
|
|
461
|
+
"./manualEditingAvailability",
|
|
462
|
+
);
|
|
463
|
+
return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: false };
|
|
464
|
+
});
|
|
451
465
|
const { PropertyPanel } = await import("./PropertyPanel");
|
|
452
466
|
const host = render(
|
|
453
467
|
<PropertyPanel
|
|
@@ -9,11 +9,17 @@ export function CommitField({
|
|
|
9
9
|
value,
|
|
10
10
|
disabled,
|
|
11
11
|
liveCommit,
|
|
12
|
+
align = "left",
|
|
12
13
|
onCommit,
|
|
13
14
|
}: {
|
|
14
15
|
value: string;
|
|
15
16
|
disabled?: boolean;
|
|
16
17
|
liveCommit?: boolean;
|
|
18
|
+
/** The legacy panel lays out label-then-value inline (left reads naturally);
|
|
19
|
+
* the flat inspector lays out label…value across a `justify-between` row,
|
|
20
|
+
* where a left-aligned value looks stranded at the edge of its own
|
|
21
|
+
* right-hand box instead of lining up with every other row's value. */
|
|
22
|
+
align?: "left" | "right";
|
|
17
23
|
onCommit: (nextValue: string) => void;
|
|
18
24
|
}) {
|
|
19
25
|
const [draft, setDraft] = useState(value);
|
|
@@ -92,7 +98,9 @@ export function CommitField({
|
|
|
92
98
|
scheduleCommit(nextDraft);
|
|
93
99
|
}}
|
|
94
100
|
title={parseNumericToken(value) ? "Scroll or use Arrow keys to adjust" : undefined}
|
|
95
|
-
className=
|
|
101
|
+
className={`min-w-0 w-full bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600 ${
|
|
102
|
+
align === "right" ? "text-right" : "text-left"
|
|
103
|
+
}`}
|
|
96
104
|
/>
|
|
97
105
|
);
|
|
98
106
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AgentChatMessageButton } from "./AgentChatMessageButton";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("AgentChatMessageButton", () => {
|
|
17
|
+
it("reports a successful copy and resets to an explicit re-copy action", async () => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
20
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
value: { writeText },
|
|
23
|
+
});
|
|
24
|
+
const onCopied = vi.fn();
|
|
25
|
+
const host = document.createElement("div");
|
|
26
|
+
document.body.append(host);
|
|
27
|
+
const root = createRoot(host);
|
|
28
|
+
act(() => {
|
|
29
|
+
root.render(<AgentChatMessageButton message="handoff" onCopied={onCopied} />);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const button = host.querySelector("button");
|
|
33
|
+
if (!button) throw new Error("copy button not rendered");
|
|
34
|
+
await act(async () => {
|
|
35
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(writeText).toHaveBeenCalledWith("handoff");
|
|
40
|
+
expect(onCopied).toHaveBeenCalledOnce();
|
|
41
|
+
expect(button.textContent).toBe("Copied — paste in your agent chat");
|
|
42
|
+
|
|
43
|
+
act(() => vi.advanceTimersByTime(3000));
|
|
44
|
+
expect(button.textContent).toBe("Copy again");
|
|
45
|
+
act(() => root.unmount());
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Button } from "../ui/Button";
|
|
3
|
+
|
|
4
|
+
export const APPLY_STORYBOARD_FEEDBACK_MESSAGE =
|
|
5
|
+
"Read the storyboard feedback I saved in .hyperframes/frame-comments.json and revise the frames.";
|
|
6
|
+
|
|
7
|
+
export function AgentChatMessageButton({
|
|
8
|
+
message,
|
|
9
|
+
label = "Copy prompt for agent",
|
|
10
|
+
onCopied,
|
|
11
|
+
}: {
|
|
12
|
+
message: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
onCopied?: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const [copyState, setCopyState] = useState<"idle" | "copied" | "again" | "failed">("idle");
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (copyState !== "copied") return;
|
|
20
|
+
const timeout = window.setTimeout(() => setCopyState("again"), 3000);
|
|
21
|
+
return () => window.clearTimeout(timeout);
|
|
22
|
+
}, [copyState]);
|
|
23
|
+
|
|
24
|
+
const copyMessage = async () => {
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(message);
|
|
27
|
+
setCopyState("copied");
|
|
28
|
+
onCopied?.();
|
|
29
|
+
} catch {
|
|
30
|
+
setCopyState("failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Button size="sm" variant="secondary" onClick={() => void copyMessage()}>
|
|
36
|
+
{copyState === "copied"
|
|
37
|
+
? "Copied — paste in your agent chat"
|
|
38
|
+
: copyState === "again"
|
|
39
|
+
? "Copy again"
|
|
40
|
+
: copyState === "failed"
|
|
41
|
+
? "Copy failed"
|
|
42
|
+
: label}
|
|
43
|
+
</Button>
|
|
44
|
+
);
|
|
45
|
+
}
|