@1agh/maude 1.4.1 → 1.4.2
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/canvas-lib.tsx +28 -6
- package/apps/studio/client/app.jsx +4 -10
- package/apps/studio/client/command-palette-match.js +35 -0
- package/apps/studio/dist/client.bundle.js +480 -480
- package/apps/studio/http.ts +27 -8
- package/apps/studio/sync/index.ts +129 -22
- package/apps/studio/whats-new.json +9 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +32 -1
|
@@ -3200,6 +3200,21 @@ export function PhotoPreviewBridge() {
|
|
|
3200
3200
|
// clobbering a NEWER edit that already resolved first.
|
|
3201
3201
|
const tokenRef = useRef<Map<string, number>>(new Map());
|
|
3202
3202
|
const bakeTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
3203
|
+
// Per-asset FETCH generation — the same guard one step earlier. Two synced
|
|
3204
|
+
// edits in a row start two sidecar fetches, and the older answer can arrive
|
|
3205
|
+
// last: applying it put the previous edit back on screen while every disk had
|
|
3206
|
+
// the newer one (surface run 2026-09-16, L13 on the hub: shown, then
|
|
3207
|
+
// reverted). A live preview counts as newer than any fetch in flight.
|
|
3208
|
+
const fetchGenRef = useRef<Map<string, number>>(new Map());
|
|
3209
|
+
const nextFetchGen = useCallback((asset: string): number => {
|
|
3210
|
+
const g = (fetchGenRef.current.get(asset) ?? 0) + 1;
|
|
3211
|
+
fetchGenRef.current.set(asset, g);
|
|
3212
|
+
return g;
|
|
3213
|
+
}, []);
|
|
3214
|
+
const isLatestFetch = useCallback(
|
|
3215
|
+
(asset: string, g: number): boolean => fetchGenRef.current.get(asset) === g,
|
|
3216
|
+
[]
|
|
3217
|
+
);
|
|
3203
3218
|
|
|
3204
3219
|
const bake = useCallback((asset: string, original: string, edit: PhotoEdit) => {
|
|
3205
3220
|
const token = (tokenRef.current.get(asset) ?? 0) + 1;
|
|
@@ -3231,6 +3246,8 @@ export function PhotoPreviewBridge() {
|
|
|
3231
3246
|
clearTimeout(timers.get(asset));
|
|
3232
3247
|
if (!edit || isDefaultEdit(edit)) {
|
|
3233
3248
|
timers.delete(asset);
|
|
3249
|
+
// A bake already rendering an earlier edit must not land over this.
|
|
3250
|
+
tokenRef.current.set(asset, (tokenRef.current.get(asset) ?? 0) + 1);
|
|
3234
3251
|
setPhotoElSrc(el, original);
|
|
3235
3252
|
return;
|
|
3236
3253
|
}
|
|
@@ -3270,17 +3287,20 @@ export function PhotoPreviewBridge() {
|
|
|
3270
3287
|
}
|
|
3271
3288
|
}
|
|
3272
3289
|
if (!asset) return; // no photo on this canvas uses that asset
|
|
3273
|
-
|
|
3290
|
+
const target = asset;
|
|
3291
|
+
const g = nextFetchGen(target);
|
|
3292
|
+
fetch(`/_api/photo-edit?asset=${encodeURIComponent(target)}`)
|
|
3274
3293
|
.then((r) => (r.ok ? r.json() : null))
|
|
3275
3294
|
.then((edit) => {
|
|
3295
|
+
if (!isLatestFetch(target, g)) return; // a newer answer owns the render
|
|
3276
3296
|
// A deleted/reset edit applies as null → restores the original src.
|
|
3277
|
-
apply(
|
|
3297
|
+
apply(target, edit && !isDefaultEdit(edit) ? (edit as PhotoEdit) : null);
|
|
3278
3298
|
})
|
|
3279
3299
|
.catch(() => {});
|
|
3280
3300
|
};
|
|
3281
3301
|
document.addEventListener('maude:photo-edit-refreshed', onRefreshed);
|
|
3282
3302
|
return () => document.removeEventListener('maude:photo-edit-refreshed', onRefreshed);
|
|
3283
|
-
}, [apply]);
|
|
3303
|
+
}, [apply, nextFetchGen, isLatestFetch]);
|
|
3284
3304
|
|
|
3285
3305
|
useEffect(() => {
|
|
3286
3306
|
const onMsg = (e: MessageEvent) => {
|
|
@@ -3304,11 +3324,12 @@ export function PhotoPreviewBridge() {
|
|
|
3304
3324
|
if (m.dgn !== 'photo-preview' || typeof m.asset !== 'string') return;
|
|
3305
3325
|
const asset = m.asset;
|
|
3306
3326
|
const edit = (m.edit ?? null) as PhotoEdit | null;
|
|
3327
|
+
nextFetchGen(asset);
|
|
3307
3328
|
apply(asset, edit);
|
|
3308
3329
|
};
|
|
3309
3330
|
window.addEventListener('message', onMsg);
|
|
3310
3331
|
return () => window.removeEventListener('message', onMsg);
|
|
3311
|
-
}, [apply]);
|
|
3332
|
+
}, [apply, nextFetchGen]);
|
|
3312
3333
|
// Boot-time (+ ongoing) hydration from the PERSISTED sidecar, not just the
|
|
3313
3334
|
// live `photo-preview` message above. Without this, a saved PhotoEdit is
|
|
3314
3335
|
// invisible after anything that re-mounts the canvas doc (Cmd+R, an
|
|
@@ -3342,10 +3363,11 @@ export function PhotoPreviewBridge() {
|
|
|
3342
3363
|
if (!asset || attempted.has(asset)) continue;
|
|
3343
3364
|
if (++scanned > MAX_SCAN_PER_PASS) break;
|
|
3344
3365
|
attempted.add(asset);
|
|
3366
|
+
const g = nextFetchGen(asset);
|
|
3345
3367
|
fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`)
|
|
3346
3368
|
.then((r) => (r.ok ? r.json() : null))
|
|
3347
3369
|
.then((edit) => {
|
|
3348
|
-
if (cancelled || !edit || isDefaultEdit(edit)) return;
|
|
3370
|
+
if (cancelled || !isLatestFetch(asset, g) || !edit || isDefaultEdit(edit)) return;
|
|
3349
3371
|
apply(asset, edit);
|
|
3350
3372
|
})
|
|
3351
3373
|
.catch(() => {});
|
|
@@ -3367,7 +3389,7 @@ export function PhotoPreviewBridge() {
|
|
|
3367
3389
|
cancelAnimationFrame(raf);
|
|
3368
3390
|
observer.disconnect();
|
|
3369
3391
|
};
|
|
3370
|
-
}, [apply]);
|
|
3392
|
+
}, [apply, nextFetchGen, isLatestFetch]);
|
|
3371
3393
|
return null; // mutates the real elements directly — no visible DOM of its own.
|
|
3372
3394
|
}
|
|
3373
3395
|
PhotoPreviewBridge.displayName = 'PhotoPreviewBridge';
|
|
@@ -12,6 +12,7 @@ import { createRoot } from 'react-dom/client';
|
|
|
12
12
|
// import that Bun erases), so this pulls only string constants into the client
|
|
13
13
|
// bundle — no React, no input-router. See the tool-cursor handler below.
|
|
14
14
|
import { resolveToolCursor } from '../canvas-cursors.ts';
|
|
15
|
+
import { commandForEnter, matchCommands } from './command-palette-match.js';
|
|
15
16
|
import {
|
|
16
17
|
defaultScopeForFormat,
|
|
17
18
|
isScopeValidForFormat,
|
|
@@ -878,15 +879,7 @@ function CommandPalette({ open, onClose, actions }) {
|
|
|
878
879
|
?.querySelector('.st-pal-item.is-active')
|
|
879
880
|
?.scrollIntoView({ block: 'nearest' });
|
|
880
881
|
}, [active]);
|
|
881
|
-
const filtered = useMemo(() =>
|
|
882
|
-
const needle = q.trim().toLowerCase();
|
|
883
|
-
if (!needle) return actions;
|
|
884
|
-
return actions.filter(
|
|
885
|
-
(a) =>
|
|
886
|
-
a.label.toLowerCase().includes(needle) ||
|
|
887
|
-
(a.group && a.group.toLowerCase().includes(needle))
|
|
888
|
-
);
|
|
889
|
-
}, [q, actions]);
|
|
882
|
+
const filtered = useMemo(() => matchCommands(actions, q), [q, actions]);
|
|
890
883
|
useEffect(() => {
|
|
891
884
|
if (active >= filtered.length) setActive(0);
|
|
892
885
|
}, [filtered.length, active]);
|
|
@@ -925,7 +918,8 @@ function CommandPalette({ open, onClose, actions }) {
|
|
|
925
918
|
setActive((i) => Math.max(0, i - 1));
|
|
926
919
|
} else if (e.key === 'Enter') {
|
|
927
920
|
e.preventDefault();
|
|
928
|
-
|
|
921
|
+
const pick = commandForEnter(actions, q, e.currentTarget.value, active);
|
|
922
|
+
if (pick) run(pick);
|
|
929
923
|
}
|
|
930
924
|
}}
|
|
931
925
|
/>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// command-palette-match.js — which ⌘K command a query names.
|
|
2
|
+
//
|
|
3
|
+
// Kept out of app.jsx so the one decision that matters — what Enter runs —
|
|
4
|
+
// can be tested on its own.
|
|
5
|
+
|
|
6
|
+
/** The commands a query matches, in list order (label or group substring). */
|
|
7
|
+
export function matchCommands(actions, query) {
|
|
8
|
+
const needle = String(query ?? '')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
if (!needle) return actions;
|
|
12
|
+
return actions.filter(
|
|
13
|
+
(a) => a.label.toLowerCase().includes(needle) || (a.group && a.group.toLowerCase().includes(needle))
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The command Enter runs.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS IN THE BOX, not what the last render saw. A query typed or pasted
|
|
21
|
+
* and confirmed at once can reach Enter before the list re-rendered for it;
|
|
22
|
+
* the rendered list is then the unfiltered one and its first row — "New brief
|
|
23
|
+
* board" — ran instead of the command that was asked for (surface run
|
|
24
|
+
* 2026-09-16, "New video" on the hub). When the box and the rendered query
|
|
25
|
+
* disagree, the box wins and its first match runs; when they agree, the
|
|
26
|
+
* highlighted row does.
|
|
27
|
+
*/
|
|
28
|
+
export function commandForEnter(actions, renderedQuery, liveQuery, activeIndex) {
|
|
29
|
+
const norm = (s) =>
|
|
30
|
+
String(s ?? '')
|
|
31
|
+
.trim()
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
if (norm(liveQuery) !== norm(renderedQuery)) return matchCommands(actions, liveQuery)[0] ?? null;
|
|
34
|
+
return matchCommands(actions, renderedQuery)[activeIndex] ?? null;
|
|
35
|
+
}
|