@hyperframes/studio 0.7.46 → 0.7.48
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-DO2MAFSG.js → index-CWHON2kh.js} +1 -1
- package/dist/assets/index-DX02Q1Yl.js +423 -0
- package/dist/assets/index-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-RTqYPUN7.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- package/dist/assets/index-DoVLXke0.css +0 -1
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
type MutableRefObject,
|
|
8
8
|
type PointerEvent as ReactPointerEvent,
|
|
9
9
|
} from "react";
|
|
10
|
-
import { Tooltip } from "./ui";
|
|
11
10
|
import { PropertyPanel } from "./editor/PropertyPanel";
|
|
12
11
|
import { LayersPanel } from "./editor/LayersPanel";
|
|
13
12
|
import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
|
|
@@ -15,6 +14,9 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
|
|
|
15
14
|
import { RenderQueue } from "./renders/RenderQueue";
|
|
16
15
|
import { SlideshowPanel } from "./panels/SlideshowPanel";
|
|
17
16
|
import type { SceneInfo } from "./panels/SlideshowPanel";
|
|
17
|
+
import { VariablesPanel } from "./panels/VariablesPanel";
|
|
18
|
+
import { PanelTabButton } from "./PanelTabButton";
|
|
19
|
+
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
|
18
20
|
import type { RenderJob } from "./renders/useRenderQueue";
|
|
19
21
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
20
22
|
import type { IframeWindow } from "../player/lib/playbackTypes";
|
|
@@ -22,6 +24,7 @@ import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailabil
|
|
|
22
24
|
import type { Composition } from "@hyperframes/sdk";
|
|
23
25
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
24
26
|
import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
|
|
27
|
+
import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
|
|
25
28
|
|
|
26
29
|
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
|
|
27
30
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
@@ -340,63 +343,74 @@ export function StudioRightPanel({
|
|
|
340
343
|
);
|
|
341
344
|
|
|
342
345
|
const propertyPanel = (
|
|
343
|
-
<
|
|
346
|
+
<DesignPanelPromoteProvider
|
|
347
|
+
selection={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
344
348
|
projectId={projectId}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
349
|
+
activeCompPath={activeCompPath}
|
|
350
|
+
readProjectFile={readProjectFile}
|
|
351
|
+
writeProjectFile={writeProjectFile}
|
|
352
|
+
recordEdit={recordEdit}
|
|
353
|
+
reloadPreview={reloadPreview}
|
|
354
|
+
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
355
|
+
>
|
|
356
|
+
<PropertyPanel
|
|
357
|
+
projectId={projectId}
|
|
358
|
+
projectDir={projectDir}
|
|
359
|
+
assets={assets}
|
|
360
|
+
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
361
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
362
|
+
copiedAgentPrompt={copiedAgentPrompt}
|
|
363
|
+
onClearSelection={clearDomSelection}
|
|
364
|
+
onToggleElementHidden={onToggleElementHidden}
|
|
365
|
+
onUngroup={handleUngroupSelection}
|
|
366
|
+
onSetStyle={handleDomStyleCommit}
|
|
367
|
+
onSetAttribute={handleDomAttributeCommit}
|
|
368
|
+
onSetAttributeLive={handleDomAttributeLiveCommit}
|
|
369
|
+
onApplyColorGradingScope={handleApplyColorGradingScope}
|
|
370
|
+
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
|
|
371
|
+
onRemoveBackground={handleRemoveBackground}
|
|
372
|
+
onSetManualOffset={handleDomPathOffsetCommit}
|
|
373
|
+
onSetManualSize={handleDomBoxSizeCommit}
|
|
374
|
+
onSetManualRotation={handleDomRotationCommit}
|
|
375
|
+
onSetText={handleDomTextCommit}
|
|
376
|
+
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
|
377
|
+
onAddTextField={handleDomAddTextField}
|
|
378
|
+
onRemoveTextField={handleDomRemoveTextField}
|
|
379
|
+
onAskAgent={handleAskAgent}
|
|
380
|
+
onImportAssets={handleImportFiles}
|
|
381
|
+
fontAssets={fontAssets}
|
|
382
|
+
onImportFonts={handleImportFonts}
|
|
383
|
+
previewIframeRef={previewIframeRef}
|
|
384
|
+
gsapAnimations={selectedGsapAnimations}
|
|
385
|
+
gsapMultipleTimelines={gsapMultipleTimelines}
|
|
386
|
+
gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
|
387
|
+
onUpdateGsapProperty={handleGsapUpdateProperty}
|
|
388
|
+
onUpdateGsapMeta={handleGsapUpdateMeta}
|
|
389
|
+
onDeleteGsapAnimation={handleGsapDeleteAnimation}
|
|
390
|
+
onAddGsapProperty={handleGsapAddProperty}
|
|
391
|
+
onRemoveGsapProperty={handleGsapRemoveProperty}
|
|
392
|
+
onUpdateGsapFromProperty={handleGsapUpdateFromProperty}
|
|
393
|
+
onAddGsapFromProperty={handleGsapAddFromProperty}
|
|
394
|
+
onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
|
|
395
|
+
onAddGsapAnimation={handleGsapAddAnimation}
|
|
396
|
+
onCommitAnimatedProperty={commitAnimatedProperty}
|
|
397
|
+
onCommitAnimatedProperties={commitAnimatedProperties}
|
|
398
|
+
onAddKeyframe={handleGsapAddKeyframe}
|
|
399
|
+
onRemoveKeyframe={handleGsapRemoveKeyframe}
|
|
400
|
+
onConvertToKeyframes={(animId, duration) =>
|
|
401
|
+
handleGsapConvertToKeyframes(animId, undefined, duration)
|
|
402
|
+
}
|
|
403
|
+
onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
|
|
404
|
+
onSetArcPath={handleSetArcPath}
|
|
405
|
+
onUpdateArcSegment={handleUpdateArcSegment}
|
|
406
|
+
onUnroll={handleUnroll}
|
|
407
|
+
onUpdateKeyframeEase={handleUpdateKeyframeEase}
|
|
408
|
+
onSetAllKeyframeEases={handleSetAllKeyframeEases}
|
|
409
|
+
recordingState={recordingState}
|
|
410
|
+
recordingDuration={recordingDuration}
|
|
411
|
+
onToggleRecording={onToggleRecording}
|
|
412
|
+
/>
|
|
413
|
+
</DesignPanelPromoteProvider>
|
|
400
414
|
);
|
|
401
415
|
|
|
402
416
|
const renderQueuePanel = (
|
|
@@ -420,6 +434,9 @@ export function StudioRightPanel({
|
|
|
420
434
|
format,
|
|
421
435
|
resolution,
|
|
422
436
|
composition,
|
|
437
|
+
// Render what the user is previewing: active variable overrides
|
|
438
|
+
// from the Variables panel ride along (undefined = defaults).
|
|
439
|
+
variables: usePreviewVariablesStore.getState().values ?? undefined,
|
|
423
440
|
});
|
|
424
441
|
}}
|
|
425
442
|
compositionDimensions={compositionDimensions}
|
|
@@ -461,64 +478,38 @@ export function StudioRightPanel({
|
|
|
461
478
|
<div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
|
|
462
479
|
{STUDIO_INSPECTOR_PANELS_ENABLED && (
|
|
463
480
|
<>
|
|
464
|
-
<
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
</button>
|
|
477
|
-
</Tooltip>
|
|
478
|
-
<Tooltip label="Composition layer stack" side="bottom">
|
|
479
|
-
<button
|
|
480
|
-
type="button"
|
|
481
|
-
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
482
|
-
aria-pressed={layersPaneOpen}
|
|
483
|
-
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
484
|
-
layersPaneOpen
|
|
485
|
-
? "bg-neutral-800 text-white"
|
|
486
|
-
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
487
|
-
}`}
|
|
488
|
-
>
|
|
489
|
-
Layers
|
|
490
|
-
</button>
|
|
491
|
-
</Tooltip>
|
|
481
|
+
<PanelTabButton
|
|
482
|
+
label="Design"
|
|
483
|
+
tooltip="Element styles and properties"
|
|
484
|
+
active={designPaneOpen}
|
|
485
|
+
onClick={() => handleInspectorPaneButtonClick("design")}
|
|
486
|
+
/>
|
|
487
|
+
<PanelTabButton
|
|
488
|
+
label="Layers"
|
|
489
|
+
tooltip="Composition layer stack"
|
|
490
|
+
active={layersPaneOpen}
|
|
491
|
+
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
492
|
+
/>
|
|
492
493
|
</>
|
|
493
494
|
)}
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
aria-pressed={rightPanelTab === "slideshow"}
|
|
513
|
-
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
514
|
-
rightPanelTab === "slideshow"
|
|
515
|
-
? "bg-neutral-800 text-white"
|
|
516
|
-
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
517
|
-
}`}
|
|
518
|
-
>
|
|
519
|
-
Slideshow
|
|
520
|
-
</button>
|
|
521
|
-
</Tooltip>
|
|
495
|
+
<PanelTabButton
|
|
496
|
+
label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
|
497
|
+
tooltip="Render queue and exports"
|
|
498
|
+
active={rightPanelTab === "renders"}
|
|
499
|
+
onClick={() => setRightPanelTab("renders")}
|
|
500
|
+
/>
|
|
501
|
+
<PanelTabButton
|
|
502
|
+
label="Slideshow"
|
|
503
|
+
tooltip="Slideshow branching editor"
|
|
504
|
+
active={rightPanelTab === "slideshow"}
|
|
505
|
+
onClick={() => setRightPanelTab("slideshow")}
|
|
506
|
+
/>
|
|
507
|
+
<PanelTabButton
|
|
508
|
+
label="Variables"
|
|
509
|
+
tooltip="Template variables — declare, preview with values"
|
|
510
|
+
active={rightPanelTab === "variables"}
|
|
511
|
+
onClick={() => setRightPanelTab("variables")}
|
|
512
|
+
/>
|
|
522
513
|
</div>
|
|
523
514
|
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
524
515
|
{rightPanelTab === "block-params" && activeBlockParams ? (
|
|
@@ -535,6 +526,13 @@ export function StudioRightPanel({
|
|
|
535
526
|
onPersist={onPersistSlideshow}
|
|
536
527
|
onPersistNotes={onPersistSlideshowNotes}
|
|
537
528
|
/>
|
|
529
|
+
) : rightPanelTab === "variables" ? (
|
|
530
|
+
<VariablesPanel
|
|
531
|
+
sdkSession={sdkSession}
|
|
532
|
+
reloadPreview={reloadPreview}
|
|
533
|
+
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
534
|
+
recordEdit={recordEdit}
|
|
535
|
+
/>
|
|
538
536
|
) : layersPaneOpen && designPaneOpen ? (
|
|
539
537
|
<div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
|
|
540
538
|
<div
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../../utils/studioHelpers";
|
|
15
15
|
import { Layers } from "../../icons/SystemIcons";
|
|
16
16
|
import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
|
|
17
|
+
import { computeReorderZValues, getElementZIndex } from "../../player/lib/layerOrdering";
|
|
17
18
|
|
|
18
19
|
const TAG_ICONS: Record<string, string> = {
|
|
19
20
|
video: "Vi",
|
|
@@ -228,9 +229,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
228
229
|
reordered.splice(toIndex, 0, moved);
|
|
229
230
|
|
|
230
231
|
const existingValues = siblingLayers.map((l) => getElementZIndex(l.element));
|
|
231
|
-
const
|
|
232
|
-
const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
|
|
233
|
-
const zValues = hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
|
|
232
|
+
const zValues = computeReorderZValues(existingValues, fromIndex, toIndex);
|
|
234
233
|
|
|
235
234
|
const entries = reordered.map((layer, i) => ({
|
|
236
235
|
element: layer.element,
|
|
@@ -388,25 +387,6 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
388
387
|
|
|
389
388
|
// ── Pure helpers ──────────────────────────────────────────────────────
|
|
390
389
|
|
|
391
|
-
// fallow-ignore-next-line complexity
|
|
392
|
-
function getElementZIndex(element: HTMLElement): number {
|
|
393
|
-
try {
|
|
394
|
-
const inline = element.style?.zIndex;
|
|
395
|
-
if (inline && inline !== "auto") {
|
|
396
|
-
const parsed = parseInt(inline, 10);
|
|
397
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
398
|
-
}
|
|
399
|
-
const win = element.ownerDocument?.defaultView;
|
|
400
|
-
if (!win) return 0;
|
|
401
|
-
const value = win.getComputedStyle(element).zIndex;
|
|
402
|
-
if (value === "auto" || value === "") return 0;
|
|
403
|
-
const parsed = parseInt(value, 10);
|
|
404
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
405
|
-
} catch {
|
|
406
|
-
return 0;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
390
|
// fallow-ignore-next-line complexity
|
|
411
391
|
export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
|
|
412
392
|
if (layers.length <= 1) return layers;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a Design-panel property control with the promote-to-variable gesture.
|
|
3
|
+
* When a control can be promoted it shows a visible "◇ var" button; clicking it
|
|
4
|
+
* declares a variable (default = current value, so the render is unchanged) and
|
|
5
|
+
* binds this property to it. Once bound, the button is replaced by a "◆ {id}"
|
|
6
|
+
* chip and edits route to the variable's default (edit-in-place). Controls that
|
|
7
|
+
* aren't eligible (or render outside a promote context, or are disabled by the
|
|
8
|
+
* caller) pass through untouched. Uses a render-prop so each control keeps its
|
|
9
|
+
* own value/onCommit shape.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect } from "react";
|
|
13
|
+
import {
|
|
14
|
+
useVariablePromoteChannel,
|
|
15
|
+
type PromoteChannel,
|
|
16
|
+
} from "../../contexts/VariablePromoteContext";
|
|
17
|
+
|
|
18
|
+
interface RenderArgs {
|
|
19
|
+
/** When bound, the variable's default to display; otherwise undefined. */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** When bound, routes commits to the variable default; otherwise undefined. */
|
|
22
|
+
onCommit?: (value: string) => void;
|
|
23
|
+
bound: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function PromotableControl({
|
|
27
|
+
channel,
|
|
28
|
+
enabled = true,
|
|
29
|
+
children,
|
|
30
|
+
}: {
|
|
31
|
+
channel: PromoteChannel;
|
|
32
|
+
/**
|
|
33
|
+
* Caller-side gate. Text-section controls only promote when the edited field
|
|
34
|
+
* is the selected element's OWN text (source "self"); binding a child/text-
|
|
35
|
+
* node field would target a different element than the control edits.
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
children: (args: RenderArgs) => React.ReactNode;
|
|
39
|
+
}) {
|
|
40
|
+
const promote = useVariablePromoteChannel(channel);
|
|
41
|
+
|
|
42
|
+
// A binding attribute (`data-var-*` / `var(--id)`) pointing at a declaration
|
|
43
|
+
// that no longer exists renders as a plain unbound control — a silent
|
|
44
|
+
// fallback that leaves a dev wondering why "their binding isn't showing".
|
|
45
|
+
// Surface it in the console so the dangling reference is discoverable.
|
|
46
|
+
const danglingId =
|
|
47
|
+
enabled && promote && promote.boundId != null && promote.declaration == null
|
|
48
|
+
? promote.boundId
|
|
49
|
+
: null;
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (danglingId != null) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk — re-declare the variable or unbind the element.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}, [danglingId]);
|
|
57
|
+
|
|
58
|
+
if (!promote || !enabled) return <>{children({ bound: false })}</>;
|
|
59
|
+
|
|
60
|
+
// A binding whose declaration was removed elsewhere is dangling: don't show
|
|
61
|
+
// it as an editable bound control (setDefault would silently no-op) — let it
|
|
62
|
+
// fall back to a plain, re-promotable control.
|
|
63
|
+
const bound = promote.boundId != null && promote.declaration != null;
|
|
64
|
+
const canPromote = promote.action != null && !bound;
|
|
65
|
+
const defaultValue = promote.declaration?.default;
|
|
66
|
+
|
|
67
|
+
const rendered = children(
|
|
68
|
+
bound
|
|
69
|
+
? {
|
|
70
|
+
// Only string defaults render inline; a FontValue/ImageValue object
|
|
71
|
+
// falls back to the element's real value instead of "[object Object]".
|
|
72
|
+
value: typeof defaultValue === "string" ? defaultValue : undefined,
|
|
73
|
+
onCommit: promote.setDefault,
|
|
74
|
+
bound: true,
|
|
75
|
+
}
|
|
76
|
+
: { bound: false },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={`relative ${bound ? "rounded-lg ring-1 ring-studio-accent/40" : ""}`}>
|
|
81
|
+
{rendered}
|
|
82
|
+
{bound && (
|
|
83
|
+
<span
|
|
84
|
+
className="pointer-events-none absolute right-1.5 top-0 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
|
|
85
|
+
title={`Bound to variable "${promote.boundId}"`}
|
|
86
|
+
>
|
|
87
|
+
◆ {promote.boundId}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
{canPromote && (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
title="Make this a variable"
|
|
94
|
+
onClick={(e) => {
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
promote.promote();
|
|
97
|
+
}}
|
|
98
|
+
className="absolute right-1.5 top-0 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
|
|
99
|
+
>
|
|
100
|
+
◇ var
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -6,6 +6,7 @@ import { FIELD, LABEL, normalizeTextMetricValue, RESPONSIVE_GRID } from "./prope
|
|
|
6
6
|
import { MetricField, Section, SelectField } from "./propertyPanelPrimitives";
|
|
7
7
|
import { ColorField } from "./propertyPanelColor";
|
|
8
8
|
import { FontFamilyField } from "./propertyPanelFont";
|
|
9
|
+
import { PromotableControl } from "./PromotableControl";
|
|
9
10
|
|
|
10
11
|
/* ------------------------------------------------------------------ */
|
|
11
12
|
/* Text helpers (used only by text section components) */
|
|
@@ -280,20 +281,31 @@ function TextFieldEditor({
|
|
|
280
281
|
</button>
|
|
281
282
|
)}
|
|
282
283
|
</div>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
284
|
+
<PromotableControl channel={{ kind: "text" }} enabled={field.source === "self"}>
|
|
285
|
+
{({ value, onCommit }) => (
|
|
286
|
+
<TextAreaField
|
|
287
|
+
key={field.key}
|
|
288
|
+
label="Content"
|
|
289
|
+
value={value ?? field.value}
|
|
290
|
+
disabled={false}
|
|
291
|
+
autoFocus={showRemove}
|
|
292
|
+
onCommit={onCommit ?? ((next) => onSetText(next, field.key))}
|
|
293
|
+
/>
|
|
294
|
+
)}
|
|
295
|
+
</PromotableControl>
|
|
296
|
+
<PromotableControl
|
|
297
|
+
channel={{ kind: "style", prop: "color" }}
|
|
298
|
+
enabled={field.source === "self"}
|
|
299
|
+
>
|
|
300
|
+
{({ value, onCommit }) => (
|
|
301
|
+
<ColorField
|
|
302
|
+
label="Text color"
|
|
303
|
+
value={value ?? getTextFieldColor(field, styles)}
|
|
304
|
+
disabled={false}
|
|
305
|
+
onCommit={onCommit ?? ((next) => onSetTextFieldStyle(field.key, "color", next))}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
</PromotableControl>
|
|
297
309
|
<div className={RESPONSIVE_GRID}>
|
|
298
310
|
<MetricField
|
|
299
311
|
label="Size"
|
|
@@ -309,13 +321,22 @@ function TextFieldEditor({
|
|
|
309
321
|
onCommit={(next) => onSetTextFieldStyle(field.key, "font-weight", next)}
|
|
310
322
|
/>
|
|
311
323
|
</div>
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
324
|
+
<PromotableControl
|
|
325
|
+
channel={{ kind: "style", prop: "font-family" }}
|
|
326
|
+
enabled={field.source === "self"}
|
|
327
|
+
>
|
|
328
|
+
{({ value, onCommit }) => (
|
|
329
|
+
<FontFamilyField
|
|
330
|
+
value={
|
|
331
|
+
value ?? (field.computedStyles["font-family"] || styles["font-family"] || "inherit")
|
|
332
|
+
}
|
|
333
|
+
disabled={false}
|
|
334
|
+
importedFonts={fontAssets}
|
|
335
|
+
onImportFonts={onImportFonts}
|
|
336
|
+
onCommit={onCommit ?? ((next) => onSetTextFieldStyle(field.key, "font-family", next))}
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
</PromotableControl>
|
|
319
340
|
<AdvancedTextControls
|
|
320
341
|
field={field}
|
|
321
342
|
inheritedStyles={styles}
|