@1agh/maude 0.58.3 → 0.60.0
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/apps/studio/annotations-layer.tsx +49 -15
- package/apps/studio/bin/_import-asset.mjs +18 -0
- package/apps/studio/bin/_import-figma.mjs +1180 -242
- package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
- package/apps/studio/bin/_perf-probe.mjs +228 -0
- package/apps/studio/bin/_perf-shared.mjs +345 -0
- package/apps/studio/bin/_video-playwright.mjs +17 -4
- package/apps/studio/bin/import-figma.sh +10 -1
- package/apps/studio/bin/perf.sh +228 -0
- package/apps/studio/bin/smoke.sh +49 -5
- package/apps/studio/canvas-lib.tsx +148 -6
- package/apps/studio/client/app.jsx +152 -37
- package/apps/studio/client/panels/SyncPanel.jsx +320 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +29 -1
- package/apps/studio/client/panels/timeline-comp-target.js +101 -0
- package/apps/studio/client/styles/3-shell-maude.css +40 -0
- package/apps/studio/client/styles/4-components.css +4 -4
- package/apps/studio/context.ts +4 -0
- package/apps/studio/dist/client.bundle.js +772 -772
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/video-encode-lib.ts +8 -5
- package/apps/studio/exporters/video.ts +10 -0
- package/apps/studio/figma/assets.test.ts +92 -0
- package/apps/studio/figma/assets.ts +63 -9
- package/apps/studio/figma/codegen-client.test.ts +276 -0
- package/apps/studio/figma/codegen-client.ts +509 -0
- package/apps/studio/figma/codegen-fonts.test.ts +103 -0
- package/apps/studio/figma/codegen-fonts.ts +195 -0
- package/apps/studio/figma/codegen-values.test.ts +179 -0
- package/apps/studio/figma/codegen-values.ts +270 -0
- package/apps/studio/figma/endpoints.ts +73 -0
- package/apps/studio/figma/fig-decode.test.ts +788 -0
- package/apps/studio/figma/fig-decode.ts +839 -0
- package/apps/studio/figma/fig-differential.test.ts +182 -0
- package/apps/studio/figma/fig-kiwi.ts +410 -0
- package/apps/studio/figma/fig-translator.test.ts +192 -0
- package/apps/studio/figma/fig-vector.test.ts +113 -0
- package/apps/studio/figma/fig-vector.ts +145 -0
- package/apps/studio/figma/fig-zip.ts +270 -0
- package/apps/studio/figma/from-codegen.test.ts +408 -0
- package/apps/studio/figma/from-codegen.ts +1103 -0
- package/apps/studio/figma/sanitize.test.ts +69 -0
- package/apps/studio/figma/sanitize.ts +146 -47
- package/apps/studio/figma/tailwind-map.test.ts +142 -0
- package/apps/studio/figma/tailwind-map.ts +545 -0
- package/apps/studio/figma/to-artboard.ts +41 -1
- package/apps/studio/figma/to-render.ts +25 -3
- package/apps/studio/figma/types.ts +6 -1
- package/apps/studio/http.ts +94 -0
- package/apps/studio/sync/asset-push-worker.ts +84 -0
- package/apps/studio/sync/asset-push.ts +441 -39
- package/apps/studio/sync/asset-sweep.ts +262 -0
- package/apps/studio/sync/connection-state.ts +71 -3
- package/apps/studio/sync/index.ts +39 -6
- package/apps/studio/sync/presentation.ts +21 -0
- package/apps/studio/sync/status.ts +18 -0
- package/apps/studio/sync/supervisor.ts +20 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
- package/apps/studio/test/figma-explode.test.ts +438 -0
- package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
- package/apps/studio/test/import-figma.test.ts +192 -4
- package/apps/studio/test/sync-asset-push-worker.test.ts +183 -0
- package/apps/studio/test/sync-asset-push.test.ts +639 -47
- package/apps/studio/test/sync-asset-sweep.test.ts +243 -0
- package/apps/studio/test/sync-connection-state.test.ts +66 -0
- package/apps/studio/test/sync-panel-surface.test.ts +123 -0
- package/apps/studio/test/sync-resync-routes.test.ts +125 -0
- package/apps/studio/test/sync-status.test.ts +28 -0
- package/apps/studio/test/sync-supervisor.test.ts +46 -0
- package/apps/studio/test/timeline-comp-target.test.ts +139 -0
- package/apps/studio/test/video-comp.test.ts +81 -1
- package/apps/studio/test/video-encode-lib.test.ts +63 -0
- package/apps/studio/use-artboard-drag.tsx +37 -3
- package/apps/studio/video-comp.tsx +51 -0
- package/apps/studio/whats-new.json +87 -0
- package/cli/commands/design.mjs +7 -0
- package/cli/commands/kg.mjs +8 -1
- package/cli/commands/kg.test.mjs +24 -0
- package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
- package/cli/lib/figma-import-controls.test.mjs +70 -0
- package/package.json +8 -8
|
@@ -33,6 +33,7 @@ import { useTreeDrag } from './use-tree-drag.js';
|
|
|
33
33
|
import ChatPanel from './panels/ChatPanel.jsx';
|
|
34
34
|
import DiffView from './panels/DiffView.jsx';
|
|
35
35
|
import GitPanel from './panels/GitPanel.jsx';
|
|
36
|
+
import SyncPanel from './panels/SyncPanel.jsx';
|
|
36
37
|
import CloudBar from './panels/CloudBar.jsx';
|
|
37
38
|
import IdentityBar from './panels/IdentityBar.jsx';
|
|
38
39
|
import OnboardingWizard from './panels/OnboardingWizard.jsx';
|
|
@@ -44,6 +45,11 @@ import { FilePreview, sanitizeDisplayText } from './panels/file-preview.jsx';
|
|
|
44
45
|
import SetupChecklistDialog, { useSetupReadiness } from './panels/SetupChecklist.jsx';
|
|
45
46
|
import TimelinePanel from './panels/TimelinePanel.jsx';
|
|
46
47
|
import { parseCompTimeline } from './panels/timeline-parse.js';
|
|
48
|
+
import {
|
|
49
|
+
activeComp,
|
|
50
|
+
resolveCompTarget,
|
|
51
|
+
sanitizeArtboardText,
|
|
52
|
+
} from './panels/timeline-comp-target.js';
|
|
47
53
|
import { durationFramesForDrop, probeMediaDuration } from './panels/timeline-media-cache.js';
|
|
48
54
|
import GenerateDialog from './generate-dialog.jsx';
|
|
49
55
|
import RepoBranchSwitcher from './panels/RepoBranchSwitcher.jsx';
|
|
@@ -149,6 +155,7 @@ const DOCK_PANELS = [
|
|
|
149
155
|
{ id: 'inspector', label: 'Inspector' },
|
|
150
156
|
{ id: 'comments', label: 'Comments' },
|
|
151
157
|
{ id: 'changes', label: 'Changes' },
|
|
158
|
+
{ id: 'sync', label: 'Sync' }, // feature-sync-progress-modal — linked projects only
|
|
152
159
|
{ id: 'assistant', label: 'Assistant' },
|
|
153
160
|
];
|
|
154
161
|
const PANEL_SIDES_DEFAULTS = {
|
|
@@ -157,6 +164,7 @@ const PANEL_SIDES_DEFAULTS = {
|
|
|
157
164
|
inspector: 'right',
|
|
158
165
|
comments: 'right',
|
|
159
166
|
changes: 'right',
|
|
167
|
+
sync: 'right',
|
|
160
168
|
assistant: 'right',
|
|
161
169
|
};
|
|
162
170
|
const PANEL_SIDES_STORE = 'mdcc-panel-sides';
|
|
@@ -1559,7 +1567,12 @@ function ExportDialog({
|
|
|
1559
1567
|
// never a silent truncation, never a surprise refusal.
|
|
1560
1568
|
const EXPORT_SAFE_FRAMES = 900;
|
|
1561
1569
|
const EXPORT_DEFAULT_CAP = 3600;
|
|
1562
|
-
|
|
1570
|
+
// The comp being exported — resolved to the artboard this dialog targets, not
|
|
1571
|
+
// to whichever comp mounted first (#75). It drives the long-comp consent
|
|
1572
|
+
// notice AND `options.maxFrames`, so a wrong pick both misstates the frame
|
|
1573
|
+
// count to the user and raises the server-side cap on behalf of a comp that
|
|
1574
|
+
// isn't the one leaving the machine.
|
|
1575
|
+
const exportComp = activeComp(comps, resolveCompTarget(comps, { artboardId: activeArtboardId }));
|
|
1563
1576
|
const exportCompFrames = exportComp?.durationInFrames || 0;
|
|
1564
1577
|
const exportCompFps = exportComp?.fps || 30;
|
|
1565
1578
|
const exportIsHeavy = card.temporal && exportCompFrames > EXPORT_SAFE_FRAMES;
|
|
@@ -4687,6 +4700,8 @@ function StatusBar({
|
|
|
4687
4700
|
onClearSelected,
|
|
4688
4701
|
syncStatus,
|
|
4689
4702
|
syncProject,
|
|
4703
|
+
syncOpen = false,
|
|
4704
|
+
onOpenSync,
|
|
4690
4705
|
changesCount = 0,
|
|
4691
4706
|
unpushed = 0,
|
|
4692
4707
|
changesOpen = false,
|
|
@@ -4727,6 +4742,19 @@ function StatusBar({
|
|
|
4727
4742
|
title: `${p.title}${detail}${p.next ? ` — ${p.next}` : ''}`,
|
|
4728
4743
|
};
|
|
4729
4744
|
})();
|
|
4745
|
+
// One body for both chip forms (button vs plain span) below.
|
|
4746
|
+
const syncSlotBody = syncSlot && (
|
|
4747
|
+
<>
|
|
4748
|
+
<span
|
|
4749
|
+
className={'st-sb-sync-dot' + (syncSlot.online ? ' is-online' : '')}
|
|
4750
|
+
aria-hidden="true"
|
|
4751
|
+
/>
|
|
4752
|
+
<span className="lbl">hub sync</span>
|
|
4753
|
+
<span className="val" title={syncSlot.title}>
|
|
4754
|
+
{syncSlot.label}
|
|
4755
|
+
</span>
|
|
4756
|
+
</>
|
|
4757
|
+
);
|
|
4730
4758
|
|
|
4731
4759
|
return (
|
|
4732
4760
|
<footer className="st-statusbar" role="contentinfo" data-testid="statusbar">
|
|
@@ -4801,19 +4829,29 @@ function StatusBar({
|
|
|
4801
4829
|
|
|
4802
4830
|
{/* P5 (Plan C) — always-on hub-sync slot (was notSyncable-only per DDR-060
|
|
4803
4831
|
/ 9.1-D). Now also surfaces the connection-state machine's queued/synced
|
|
4804
|
-
counter for the common linked case. Solo projects render nothing.
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
{
|
|
4832
|
+
counter for the common linked case. Solo projects render nothing.
|
|
4833
|
+
feature-sync-progress-modal — a BUTTON that toggles the per-file Sync
|
|
4834
|
+
panel (like the Changes chip toggles GitPanel); the plain-span form
|
|
4835
|
+
stays for servers/sessions that pass no handler. */}
|
|
4836
|
+
{syncSlot &&
|
|
4837
|
+
(onOpenSync ? (
|
|
4838
|
+
<button
|
|
4839
|
+
type="button"
|
|
4840
|
+
className={'st-sb-slot st-sb-sync st-sb-sync--btn' + (syncOpen ? ' is-open' : '')}
|
|
4841
|
+
onClick={onOpenSync}
|
|
4842
|
+
data-testid="open-sync"
|
|
4843
|
+
data-tip="Open Sync panel"
|
|
4844
|
+
data-tip-pos="top"
|
|
4845
|
+
aria-label="Open Sync panel"
|
|
4846
|
+
aria-pressed={syncOpen}
|
|
4847
|
+
>
|
|
4848
|
+
{syncSlotBody}
|
|
4849
|
+
</button>
|
|
4850
|
+
) : (
|
|
4851
|
+
<span className="st-sb-slot st-sb-sync" role="group" aria-label="Hub sync">
|
|
4852
|
+
{syncSlotBody}
|
|
4814
4853
|
</span>
|
|
4815
|
-
|
|
4816
|
-
)}
|
|
4854
|
+
))}
|
|
4817
4855
|
|
|
4818
4856
|
{/* Which release this is. Reading it used to mean probing production by
|
|
4819
4857
|
hand — and the desktop, the browser and a cloud tab all serve the same
|
|
@@ -9495,6 +9533,10 @@ function App() {
|
|
|
9495
9533
|
// tick, and after each git action — never on the per-edit WS path.
|
|
9496
9534
|
const [remoteSync, setRemoteSync] = useState(null); // { remoteAhead, behind } | null
|
|
9497
9535
|
const [changesOpen, setChangesOpen] = useState(false);
|
|
9536
|
+
// feature-sync-progress-modal — the per-file Sync panel; toggled from the
|
|
9537
|
+
// status-bar HUB SYNC chip. Only available while `syncStatus` is non-null
|
|
9538
|
+
// (a linked project), mirroring how `changes` gates on `gitStatus.repo`.
|
|
9539
|
+
const [syncPanelOpen, setSyncPanelOpen] = useState(false);
|
|
9498
9540
|
const [diffTarget, setDiffTarget] = useState(null);
|
|
9499
9541
|
const [search, setSearch] = useState('');
|
|
9500
9542
|
const [systemData, setSystemData] = useState(null);
|
|
@@ -9738,18 +9780,6 @@ function App() {
|
|
|
9738
9780
|
// Task 5 — seam transitions between series beats ({ afterIndex, dur }).
|
|
9739
9781
|
const [timelineTransitions, setTimelineTransitions] = useState([]);
|
|
9740
9782
|
const [timelineTotal, setTimelineTotal] = useState(0);
|
|
9741
|
-
// On a multi-comp canvas the Timeline drives ONE comp (the parser scopes to it
|
|
9742
|
-
// + reports its total). Match that total back to the announced comp id so the
|
|
9743
|
-
// transport/scrub targets ONLY that comp — not every artboard on the canvas.
|
|
9744
|
-
const timelineCompId = useMemo(() => {
|
|
9745
|
-
if (!activeComps.length) return null;
|
|
9746
|
-
const match = activeComps.find((c) => c.durationInFrames === timelineTotal);
|
|
9747
|
-
return (match ?? activeComps[0]).id;
|
|
9748
|
-
}, [activeComps, timelineTotal]);
|
|
9749
|
-
const timelineCompIdRef = useRef(null);
|
|
9750
|
-
useEffect(() => {
|
|
9751
|
-
timelineCompIdRef.current = timelineCompId;
|
|
9752
|
-
}, [timelineCompId]);
|
|
9753
9783
|
// rca/issue-video-artboard-frame-reset-on-edit — last known playhead, read
|
|
9754
9784
|
// from the `timeline-comps` handler below (which fires on every comp
|
|
9755
9785
|
// (re)mount, incl. a ⌘R hard iframe reload) to re-seed the fresh Player
|
|
@@ -9769,6 +9799,34 @@ function App() {
|
|
|
9769
9799
|
useEffect(() => {
|
|
9770
9800
|
timelineArtboardIdRef.current = timelineArtboardId;
|
|
9771
9801
|
}, [timelineArtboardId]);
|
|
9802
|
+
// On a multi-comp canvas the Timeline drives ONE comp: the one mounted in the
|
|
9803
|
+
// artboard whose rows the panel is drawing. Resolved by artboard identity
|
|
9804
|
+
// (announced by video-comp.tsx), NOT by duration — two comps of equal length
|
|
9805
|
+
// made the old duration match return the first artboard's Player, so scrub +
|
|
9806
|
+
// playback moved a comp the user wasn't even looking at (issue #75).
|
|
9807
|
+
const timelineCompId = useMemo(
|
|
9808
|
+
() =>
|
|
9809
|
+
resolveCompTarget(activeComps, {
|
|
9810
|
+
// Both signals, in confidence order: the artboard the ROW PARSER scoped
|
|
9811
|
+
// to (lexical, from the .tsx) then the viewport-active one canvas-lib
|
|
9812
|
+
// reports on pan (structural). See resolveCompTarget — when the two
|
|
9813
|
+
// disagree, the one that names a comp actually mounted there wins.
|
|
9814
|
+
artboardId: [timelineArtboardId, canvasActiveArtboard],
|
|
9815
|
+
total: timelineTotal,
|
|
9816
|
+
}),
|
|
9817
|
+
[activeComps, timelineArtboardId, canvasActiveArtboard, timelineTotal]
|
|
9818
|
+
);
|
|
9819
|
+
const timelineCompIdRef = useRef(null);
|
|
9820
|
+
useEffect(() => {
|
|
9821
|
+
timelineCompIdRef.current = timelineCompId;
|
|
9822
|
+
}, [timelineCompId]);
|
|
9823
|
+
// Frame math (drop position, split, overlay insert) must use the timebase of
|
|
9824
|
+
// the comp the Timeline is actually on — `activeComps[0].fps` silently used
|
|
9825
|
+
// the first artboard's (issue #75).
|
|
9826
|
+
const timelineFps = useMemo(
|
|
9827
|
+
() => activeComp(activeComps, timelineCompId)?.fps || 30,
|
|
9828
|
+
[activeComps, timelineCompId]
|
|
9829
|
+
);
|
|
9772
9830
|
// feature-enhanced-video-editing (Task 3) — the timeline's single-select model.
|
|
9773
9831
|
// Holds the selected clip's stableId (NOT a row index — DDR-150), so the
|
|
9774
9832
|
// selection survives comp-clips refetches, reorders, and external file edits.
|
|
@@ -9876,8 +9934,8 @@ function App() {
|
|
|
9876
9934
|
const anyRightPanelOpenRef = useRef(false);
|
|
9877
9935
|
useEffect(() => {
|
|
9878
9936
|
anyRightPanelOpenRef.current =
|
|
9879
|
-
inspectorOpen || commentsPanelOpen || changesOpen || assistantOpen;
|
|
9880
|
-
}, [inspectorOpen, commentsPanelOpen, changesOpen, assistantOpen]);
|
|
9937
|
+
inspectorOpen || commentsPanelOpen || changesOpen || syncPanelOpen || assistantOpen;
|
|
9938
|
+
}, [inspectorOpen, commentsPanelOpen, changesOpen, syncPanelOpen, assistantOpen]);
|
|
9881
9939
|
// The right dock holds exactly ONE panel (Changes / Inspector / Comments) at
|
|
9882
9940
|
// a time — opening any panel REPLACES whatever was there. These two helpers
|
|
9883
9941
|
// are the single source of that invariant; every open/toggle path routes
|
|
@@ -9899,6 +9957,7 @@ function App() {
|
|
|
9899
9957
|
inspector: inspectorOpen,
|
|
9900
9958
|
comments: commentsPanelOpen,
|
|
9901
9959
|
changes: changesOpen,
|
|
9960
|
+
sync: syncPanelOpen,
|
|
9902
9961
|
assistant: assistantOpen,
|
|
9903
9962
|
},
|
|
9904
9963
|
side: panelSide,
|
|
@@ -9909,6 +9968,7 @@ function App() {
|
|
|
9909
9968
|
else if (id === 'inspector') setInspectorOpen(val);
|
|
9910
9969
|
else if (id === 'comments') setCommentsPanelOpen(val);
|
|
9911
9970
|
else if (id === 'changes') setChangesOpen(val);
|
|
9971
|
+
else if (id === 'sync') setSyncPanelOpen(val);
|
|
9912
9972
|
else if (id === 'assistant') setAssistantOpen(val);
|
|
9913
9973
|
}, []);
|
|
9914
9974
|
const sideOf = useCallback(
|
|
@@ -10443,7 +10503,9 @@ function App() {
|
|
|
10443
10503
|
// inputs stay exempt.
|
|
10444
10504
|
if (tag === 'INPUT' && t?.type !== 'range') return;
|
|
10445
10505
|
if (tag === 'TEXTAREA' || tag === 'SELECT' || t?.isContentEditable) return;
|
|
10446
|
-
|
|
10506
|
+
// The timebase of the comp the transport is on, not of whichever comp
|
|
10507
|
+
// announced first — Shift+arrow is "±1 second" on THIS artboard (#75).
|
|
10508
|
+
const fps = activeComp(s.comps, s.compId)?.fps || 30;
|
|
10447
10509
|
const total = Math.max(1, s.total);
|
|
10448
10510
|
const doSeek = (f) => {
|
|
10449
10511
|
const nf = Math.max(0, Math.min(total - 1, Math.round(f)));
|
|
@@ -10625,8 +10687,13 @@ function App() {
|
|
|
10625
10687
|
setTimelineTotal(0);
|
|
10626
10688
|
return;
|
|
10627
10689
|
}
|
|
10628
|
-
const total = activeComps[0]?.durationInFrames || 0;
|
|
10629
10690
|
const artboard = canvasActiveArtboard ?? selected?.artboardId ?? null;
|
|
10691
|
+
// Seed the parser's fallback total from the comp mounted in THIS artboard,
|
|
10692
|
+
// not from whichever comp announced first (issue #75).
|
|
10693
|
+
const seedComp =
|
|
10694
|
+
(artboard && activeComps.find((c) => c.artboardId === artboard || c.id === artboard)) ||
|
|
10695
|
+
activeComps[0];
|
|
10696
|
+
const total = seedComp?.durationInFrames || 0;
|
|
10630
10697
|
const parsed = parseCompTimeline(timelineSource, total, artboard);
|
|
10631
10698
|
setTimelineArtboardId(parsed.artboardId ?? artboard ?? null);
|
|
10632
10699
|
// Overlay the enumerator's authoritative media (kind badge + replace target)
|
|
@@ -11035,13 +11102,30 @@ function App() {
|
|
|
11035
11102
|
|
|
11036
11103
|
// ----- WebSocket -----
|
|
11037
11104
|
useEffect(() => {
|
|
11105
|
+
// KEEPALIVE. The inspector feed only pushes on events (a comment, a
|
|
11106
|
+
// selection, sync:status), so an idle designer's socket exchanges nothing
|
|
11107
|
+
// for minutes — and Bun.serve closes a WebSocket after `idleTimeout` (120s
|
|
11108
|
+
// default) with no message in EITHER direction. The client then reconnected
|
|
11109
|
+
// in a 1 s loop, so the status bar read `reconnecting` every couple of
|
|
11110
|
+
// minutes on a perfectly healthy link ("porad reconnecting" while HUB SYNC
|
|
11111
|
+
// said synced — the two are different sockets). A cheap app-level ping every
|
|
11112
|
+
// 25 s keeps the socket active (Bun resets the idle timer on any frame,
|
|
11113
|
+
// inbound OR outbound); the server ignores an unknown message type.
|
|
11114
|
+
let pingTimer = null;
|
|
11038
11115
|
function connect() {
|
|
11039
11116
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
11040
11117
|
const ws = new WebSocket(proto + '//' + location.host + '/_ws');
|
|
11041
11118
|
wsRef.current = ws;
|
|
11042
|
-
ws.addEventListener('open', () =>
|
|
11119
|
+
ws.addEventListener('open', () => {
|
|
11120
|
+
setWsConnected(true);
|
|
11121
|
+
clearInterval(pingTimer);
|
|
11122
|
+
pingTimer = setInterval(() => {
|
|
11123
|
+
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"ping"}');
|
|
11124
|
+
}, 25000);
|
|
11125
|
+
});
|
|
11043
11126
|
ws.addEventListener('close', () => {
|
|
11044
11127
|
setWsConnected(false);
|
|
11128
|
+
clearInterval(pingTimer);
|
|
11045
11129
|
setTimeout(connect, 1000);
|
|
11046
11130
|
});
|
|
11047
11131
|
ws.addEventListener('error', () => {});
|
|
@@ -11161,7 +11245,10 @@ function App() {
|
|
|
11161
11245
|
});
|
|
11162
11246
|
}
|
|
11163
11247
|
connect();
|
|
11164
|
-
return () =>
|
|
11248
|
+
return () => {
|
|
11249
|
+
clearInterval(pingTimer);
|
|
11250
|
+
if (wsRef.current) wsRef.current.close();
|
|
11251
|
+
};
|
|
11165
11252
|
// loadTree + loadServerConfig are stable useCallback([])s; listed so the
|
|
11166
11253
|
// canvas-list-update / config-updated handlers always call the live refs.
|
|
11167
11254
|
}, [loadTree, loadServerConfig]);
|
|
@@ -12509,8 +12596,10 @@ function App() {
|
|
|
12509
12596
|
? iframesRef.current.get(activePath)?.contentWindow
|
|
12510
12597
|
: null;
|
|
12511
12598
|
if (e.source !== activeWin) return;
|
|
12512
|
-
//
|
|
12513
|
-
|
|
12599
|
+
// Neutralize + cap exactly like the sibling timeline-comps handler —
|
|
12600
|
+
// this id is matched AGAINST that one, so both sides must go through
|
|
12601
|
+
// the same normalization or an odd-but-legal id would never match.
|
|
12602
|
+
setCanvasActiveArtboard(sanitizeArtboardText(m.id));
|
|
12514
12603
|
} else if (m.dgn === 'timeline-comps' && Array.isArray(m.comps)) {
|
|
12515
12604
|
// DDR-148 — a video-comp announces its comp meta (from video-comp.tsx).
|
|
12516
12605
|
// Gate to the ACTIVE canvas window (phase-28 F-2 pattern): a background
|
|
@@ -12530,6 +12619,13 @@ function App() {
|
|
|
12530
12619
|
durationInFrames: Math.max(1, Math.min(1_000_000, Math.round(Number(c.durationInFrames) || 1))),
|
|
12531
12620
|
width: Math.max(1, Math.round(Number(c.width) || 0)),
|
|
12532
12621
|
height: Math.max(1, Math.round(Number(c.height) || 0)),
|
|
12622
|
+
// The enclosing artboard (issue #75) — the key the transport target
|
|
12623
|
+
// is resolved on, and (the label) shell chrome the user reads. This
|
|
12624
|
+
// is untrusted canvas-origin text, so neutralize the bidi/zero-width
|
|
12625
|
+
// class HERE, at the boundary, and every downstream consumer
|
|
12626
|
+
// inherits a clean value (security-review 2026-08-12).
|
|
12627
|
+
artboardId: sanitizeArtboardText(c.artboardId),
|
|
12628
|
+
artboardLabel: sanitizeArtboardText(c.artboardLabel),
|
|
12533
12629
|
}));
|
|
12534
12630
|
setActiveComps(safe);
|
|
12535
12631
|
// rca/issue-video-artboard-frame-reset-on-edit — the SAME comp
|
|
@@ -14240,6 +14336,9 @@ function App() {
|
|
|
14240
14336
|
// CHANGE, not cannot SEE: a reviewer needs structure and measured values,
|
|
14241
14337
|
// which is the whole reason C1 exists.
|
|
14242
14338
|
if (viewerMode && id === 'assistant') return false;
|
|
14339
|
+
// feature-sync-progress-modal — the Sync panel only exists for a linked
|
|
14340
|
+
// project (solo has no hub, so the tab would open onto nothing).
|
|
14341
|
+
if (id === 'sync') return !!syncStatus;
|
|
14243
14342
|
return id === 'assistant' ? isNativeApp() : id === 'layers' ? layersMode === 'separate' : true;
|
|
14244
14343
|
};
|
|
14245
14344
|
const idsForSide = (side) =>
|
|
@@ -14252,6 +14351,7 @@ function App() {
|
|
|
14252
14351
|
inspector: inspectorOpen,
|
|
14253
14352
|
comments: commentsPanelOpen,
|
|
14254
14353
|
changes: changesOpen,
|
|
14354
|
+
sync: syncPanelOpen,
|
|
14255
14355
|
assistant: assistantOpen,
|
|
14256
14356
|
};
|
|
14257
14357
|
// Per-shell overrides for the dock tab strip. In a cell the `changes` panel
|
|
@@ -14369,6 +14469,16 @@ function App() {
|
|
|
14369
14469
|
designRel={(cfg?.designRel || cfg?.designRoot || '.design').replace(/^\/+|\/+$/g, '')}
|
|
14370
14470
|
/>
|
|
14371
14471
|
);
|
|
14472
|
+
if (id === 'sync')
|
|
14473
|
+
return (
|
|
14474
|
+
<SyncPanel
|
|
14475
|
+
status={syncStatus}
|
|
14476
|
+
project={cfg?.cloud?.projectName || project}
|
|
14477
|
+
groupPaths={(groups || []).map((g) => g.path).filter(Boolean)}
|
|
14478
|
+
resizing={resizingFor('sync')}
|
|
14479
|
+
onClose={() => setSyncPanelOpen(false)}
|
|
14480
|
+
/>
|
|
14481
|
+
);
|
|
14372
14482
|
if (id === 'inspector' || id === 'layers')
|
|
14373
14483
|
return (
|
|
14374
14484
|
<InspectorPanel
|
|
@@ -14703,6 +14813,7 @@ function App() {
|
|
|
14703
14813
|
{timelineOpen && (
|
|
14704
14814
|
<TimelinePanel
|
|
14705
14815
|
comps={activeComps}
|
|
14816
|
+
compId={timelineCompId}
|
|
14706
14817
|
sequences={timelineSequences}
|
|
14707
14818
|
audio={timelineAudio}
|
|
14708
14819
|
transitions={timelineTransitions}
|
|
@@ -14981,7 +15092,7 @@ function App() {
|
|
|
14981
15092
|
// files always land in the audio band. Without a position, the
|
|
14982
15093
|
// legacy append path.
|
|
14983
15094
|
const artboardId = timelineArtboardId || undefined;
|
|
14984
|
-
const fps =
|
|
15095
|
+
const fps = timelineFps;
|
|
14985
15096
|
const canvas = activePath;
|
|
14986
15097
|
const list = (Array.isArray(files) ? files : [files]).filter(Boolean);
|
|
14987
15098
|
(async () => {
|
|
@@ -15195,7 +15306,7 @@ function App() {
|
|
|
15195
15306
|
// happens in the timeline inspector).
|
|
15196
15307
|
Promise.resolve('Title').then((text) => {
|
|
15197
15308
|
if (!text) return;
|
|
15198
|
-
const fps =
|
|
15309
|
+
const fps = timelineFps;
|
|
15199
15310
|
fetch('/_api/insert-sequence', {
|
|
15200
15311
|
method: 'POST',
|
|
15201
15312
|
headers: { 'content-type': 'application/json' },
|
|
@@ -15229,7 +15340,7 @@ function App() {
|
|
|
15229
15340
|
(async () => {
|
|
15230
15341
|
const prompt = 'Describe this shot — double-click to edit';
|
|
15231
15342
|
const kind = 'veo';
|
|
15232
|
-
const fps =
|
|
15343
|
+
const fps = timelineFps;
|
|
15233
15344
|
fetch('/_api/insert-sequence', {
|
|
15234
15345
|
method: 'POST',
|
|
15235
15346
|
headers: { 'content-type': 'application/json' },
|
|
@@ -15336,7 +15447,7 @@ function App() {
|
|
|
15336
15447
|
// overlay-lane <Img> at the playhead. WKWebView can't open an
|
|
15337
15448
|
// HTML file input, so native goes through the Rust pick dialog.
|
|
15338
15449
|
const uploadPicked = (blob, type) => {
|
|
15339
|
-
const fps =
|
|
15450
|
+
const fps = timelineFps;
|
|
15340
15451
|
fetch('/_api/asset', {
|
|
15341
15452
|
method: 'POST',
|
|
15342
15453
|
headers: { 'Content-Type': type || 'application/octet-stream' },
|
|
@@ -15404,6 +15515,10 @@ function App() {
|
|
|
15404
15515
|
onClearSelected={clearSelected}
|
|
15405
15516
|
syncStatus={syncStatus}
|
|
15406
15517
|
syncProject={cfg?.cloud?.projectName || project}
|
|
15518
|
+
syncOpen={syncPanelOpen}
|
|
15519
|
+
// Toggle through the dock helpers so the one-panel-per-side invariant
|
|
15520
|
+
// holds (opening Sync closes whatever else the right slot shows).
|
|
15521
|
+
onOpenSync={syncStatus ? () => toggleRightPanel('sync') : undefined}
|
|
15407
15522
|
changesCount={unsavedCount}
|
|
15408
15523
|
unpushed={gitStatus?.unpushed || 0}
|
|
15409
15524
|
changesOpen={changesOpen}
|