@1agh/maude 1.4.4 → 1.4.6
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-model.ts +33 -1
- package/apps/studio/api.ts +19 -0
- package/apps/studio/canvas-comment-mount.tsx +48 -3
- package/apps/studio/canvas-lib.tsx +5 -0
- package/apps/studio/canvas-shell.tsx +110 -14
- package/apps/studio/canvas-source-memo.ts +39 -0
- package/apps/studio/canvas-text-patch.ts +117 -0
- package/apps/studio/client/app.jsx +108 -60
- package/apps/studio/client/comments-overlay.css +10 -0
- package/apps/studio/client/file-tree.jsx +157 -0
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/panels/DiffView.jsx +37 -8
- package/apps/studio/client/panels/GitPanel.jsx +2 -1
- package/apps/studio/client/panels/SourceConflictPanel.jsx +19 -17
- package/apps/studio/client/styles/3-shell-maude.css +21 -9
- package/apps/studio/client/styles/4-components.css +4 -2
- package/apps/studio/collab/index.ts +9 -0
- package/apps/studio/collab/persistence.ts +49 -1
- package/apps/studio/comment-anchor.ts +116 -0
- package/apps/studio/comments-overlay.tsx +78 -35
- package/apps/studio/context.ts +1 -0
- package/apps/studio/cursors-overlay.tsx +181 -107
- package/apps/studio/dist/client.bundle.js +1086 -1086
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/hmr-broadcast.ts +59 -3
- package/apps/studio/http.ts +3 -0
- package/apps/studio/sync/accepted-cold-start.ts +95 -7
- package/apps/studio/sync/accepted-link.ts +165 -29
- package/apps/studio/sync/agent.ts +28 -2
- package/apps/studio/sync/comment-ledger.ts +229 -0
- package/apps/studio/sync/file-plane.ts +101 -2
- package/apps/studio/sync/index.ts +124 -17
- package/apps/studio/sync/journal-client.ts +9 -3
- package/apps/studio/sync/migrate-seed.ts +9 -2
- package/apps/studio/sync/projection.ts +114 -11
- package/apps/studio/sync/remote-docs.ts +3 -2
- package/apps/studio/sync/source-recovery.ts +67 -0
- package/apps/studio/sync/status.ts +11 -3
- package/apps/studio/sync/transaction-client.ts +49 -4
- package/apps/studio/use-collab.tsx +28 -1
- package/apps/studio/use-selection-set.tsx +13 -3
- package/apps/studio/use-undo-stack.tsx +22 -2
- package/apps/studio/whats-new.json +45 -0
- package/cli/lib/workspace-plan.mjs +10 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +21 -0
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
recordBrowserExport,
|
|
47
47
|
} from './export-lane.js';
|
|
48
48
|
import { TreeRowMenu, useRowMenu } from './tree-row-menu.jsx';
|
|
49
|
+
import { FileTree, FileTreeItem } from './file-tree.jsx';
|
|
49
50
|
import { useTreeDrag } from './use-tree-drag.js';
|
|
50
51
|
import ChatPanel from './panels/ChatPanel.jsx';
|
|
51
52
|
import DiffView from './panels/DiffView.jsx';
|
|
@@ -2199,10 +2200,8 @@ function DirRow({ name, depth, defaultOpen, children, dirPath, drag, menu }) {
|
|
|
2199
2200
|
</button>
|
|
2200
2201
|
);
|
|
2201
2202
|
return (
|
|
2202
|
-
<
|
|
2203
|
-
{menu ? (
|
|
2204
|
-
<div className="st-row-wrap" role="none">
|
|
2205
|
-
{row}
|
|
2203
|
+
<FileTreeItem label={name} row={row} expanded={open} busy={isBusy}
|
|
2204
|
+
onToggle={() => setOpen(v => !v)} actions={menu ? (
|
|
2206
2205
|
<button
|
|
2207
2206
|
type="button"
|
|
2208
2207
|
className="st-row-menu-btn"
|
|
@@ -2214,12 +2213,9 @@ function DirRow({ name, depth, defaultOpen, children, dirPath, drag, menu }) {
|
|
|
2214
2213
|
>
|
|
2215
2214
|
<Icon d="M12 6a1 1 0 100-2 1 1 0 000 2zM12 13a1 1 0 100-2 1 1 0 000 2zM12 20a1 1 0 100-2 1 1 0 000 2z" size={12} />
|
|
2216
2215
|
</button>
|
|
2217
|
-
|
|
2218
|
-
) : (
|
|
2219
|
-
row
|
|
2220
|
-
)}
|
|
2216
|
+
) : null}>
|
|
2221
2217
|
{open && children}
|
|
2222
|
-
</
|
|
2218
|
+
</FileTreeItem>
|
|
2223
2219
|
);
|
|
2224
2220
|
}
|
|
2225
2221
|
|
|
@@ -2230,12 +2226,11 @@ function DirRow({ name, depth, defaultOpen, children, dirPath, drag, menu }) {
|
|
|
2230
2226
|
function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, children }) {
|
|
2231
2227
|
const [open, setOpen] = useState(defaultOpen);
|
|
2232
2228
|
return (
|
|
2233
|
-
<
|
|
2229
|
+
<FileTreeItem label={name} expanded={open} selected={active}
|
|
2230
|
+
onToggle={() => setOpen(v => !v)} row={
|
|
2234
2231
|
<div
|
|
2235
2232
|
className={'st-row st-ds-folder' + (active ? ' is-sel' : '')}
|
|
2236
2233
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
2237
|
-
role="treeitem"
|
|
2238
|
-
aria-expanded={open}
|
|
2239
2234
|
>
|
|
2240
2235
|
<button
|
|
2241
2236
|
type="button"
|
|
@@ -2249,6 +2244,8 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
2249
2244
|
<button
|
|
2250
2245
|
type="button"
|
|
2251
2246
|
className="st-ds-open"
|
|
2247
|
+
data-tree-primary=""
|
|
2248
|
+
tabIndex={-1}
|
|
2252
2249
|
onClick={() => onOpenSystem(dsName)}
|
|
2253
2250
|
aria-label={`Open ${dsName} design system view`}
|
|
2254
2251
|
title="Open the design system view"
|
|
@@ -2258,9 +2255,9 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
2258
2255
|
</span>
|
|
2259
2256
|
<span className="st-row-name">{name}</span>
|
|
2260
2257
|
</button>
|
|
2261
|
-
</div>
|
|
2258
|
+
</div>}>
|
|
2262
2259
|
{open && children}
|
|
2263
|
-
</
|
|
2260
|
+
</FileTreeItem>
|
|
2264
2261
|
);
|
|
2265
2262
|
}
|
|
2266
2263
|
|
|
@@ -2392,12 +2389,10 @@ function FileRow({
|
|
|
2392
2389
|
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
2393
2390
|
</button>
|
|
2394
2391
|
);
|
|
2395
|
-
|
|
2396
|
-
// Sibling menu/delete buttons (can't nest a button in the row button). The
|
|
2397
|
-
// wrapper is presentational so the treeitem stays the tree's child for a11y.
|
|
2392
|
+
// The named treeitem owns the primary action and its independent buttons.
|
|
2398
2393
|
return (
|
|
2399
|
-
<
|
|
2400
|
-
{
|
|
2394
|
+
<FileTreeItem label={label} row={row} selected={isSel} disabled={inert} busy={isBusy}
|
|
2395
|
+
actions={<>
|
|
2401
2396
|
{canShare && (
|
|
2402
2397
|
<button
|
|
2403
2398
|
type="button"
|
|
@@ -2434,7 +2429,7 @@ function FileRow({
|
|
|
2434
2429
|
<Icon d="M3 6h18 M8 6V4h8v2 M6 6l1 14h10l1-14 M10 11v6 M14 11v6" size={12} />
|
|
2435
2430
|
</button>
|
|
2436
2431
|
)}
|
|
2437
|
-
|
|
2432
|
+
</>} />
|
|
2438
2433
|
);
|
|
2439
2434
|
}
|
|
2440
2435
|
|
|
@@ -2557,10 +2552,9 @@ function CanvasRow({
|
|
|
2557
2552
|
</button>
|
|
2558
2553
|
);
|
|
2559
2554
|
return (
|
|
2560
|
-
<
|
|
2561
|
-
{
|
|
2562
|
-
|
|
2563
|
-
{row}
|
|
2555
|
+
<FileTreeItem label={displayName(primary.name)} row={row} selected={isSel}
|
|
2556
|
+
expanded={open} busy={isBusy} onToggle={() => setOpenState(v => !v)}
|
|
2557
|
+
actions={canShare ? (
|
|
2564
2558
|
<button
|
|
2565
2559
|
type="button"
|
|
2566
2560
|
className="st-row-menu-btn"
|
|
@@ -2572,10 +2566,7 @@ function CanvasRow({
|
|
|
2572
2566
|
>
|
|
2573
2567
|
<Icon d="M12 6a1 1 0 100-2 1 1 0 000 2zM12 13a1 1 0 100-2 1 1 0 000 2zM12 20a1 1 0 100-2 1 1 0 000 2z" size={12} />
|
|
2574
2568
|
</button>
|
|
2575
|
-
|
|
2576
|
-
) : (
|
|
2577
|
-
row
|
|
2578
|
-
)}
|
|
2569
|
+
) : null}>
|
|
2579
2570
|
{open &&
|
|
2580
2571
|
sidecars.map((sc) => (
|
|
2581
2572
|
<FileRow
|
|
@@ -2592,7 +2583,7 @@ function CanvasRow({
|
|
|
2592
2583
|
sidecar
|
|
2593
2584
|
/>
|
|
2594
2585
|
))}
|
|
2595
|
-
</
|
|
2586
|
+
</FileTreeItem>
|
|
2596
2587
|
);
|
|
2597
2588
|
}
|
|
2598
2589
|
|
|
@@ -3201,7 +3192,7 @@ function Sidebar({
|
|
|
3201
3192
|
</div>
|
|
3202
3193
|
</div>
|
|
3203
3194
|
|
|
3204
|
-
<
|
|
3195
|
+
<FileTree aria-label="Project file tree" data-testid="canvas-list">
|
|
3205
3196
|
{filteredGroups.map((g) => {
|
|
3206
3197
|
// Hide gitignored runtime / orphan-only project sections by default.
|
|
3207
3198
|
// Active search overrides — if the user typed a query, they want hits
|
|
@@ -3235,7 +3226,8 @@ function Sidebar({
|
|
|
3235
3226
|
const rootDropHandlers = canDropOnRoot ? treeDrag.dropProps(g.fullPath, true) : {};
|
|
3236
3227
|
const isRootOver = treeDrag.overDir === g.fullPath;
|
|
3237
3228
|
return (
|
|
3238
|
-
<
|
|
3229
|
+
<FileTreeItem className="st-tree-section" key={g.label} label={meta.title}
|
|
3230
|
+
expanded={sectionOpen} onToggle={() => onToggleSection(g.label, defaultOpen)} row={
|
|
3239
3231
|
<button
|
|
3240
3232
|
type="button"
|
|
3241
3233
|
className={'st-tree-sec-hd' + (isRootOver ? ' is-drop-target' : '')}
|
|
@@ -3249,7 +3241,7 @@ function Sidebar({
|
|
|
3249
3241
|
<StIcon name="chevron-right" className={'st-chev' + (sectionOpen ? ' is-open' : '')} size={13} />
|
|
3250
3242
|
<span className="st-sec-name">{meta.title}</span>
|
|
3251
3243
|
{pill && <span className="st-pill">{pill}</span>}
|
|
3252
|
-
</button>
|
|
3244
|
+
</button>}>
|
|
3253
3245
|
{sectionOpen &&
|
|
3254
3246
|
(hasItems ? (
|
|
3255
3247
|
<Tree
|
|
@@ -3276,10 +3268,10 @@ function Sidebar({
|
|
|
3276
3268
|
) : (
|
|
3277
3269
|
<div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
3278
3270
|
))}
|
|
3279
|
-
</
|
|
3271
|
+
</FileTreeItem>
|
|
3280
3272
|
);
|
|
3281
3273
|
})}
|
|
3282
|
-
</
|
|
3274
|
+
</FileTree>
|
|
3283
3275
|
<TreeRowMenu
|
|
3284
3276
|
state={rowMenu.state}
|
|
3285
3277
|
onClose={rowMenu.close}
|
|
@@ -4386,7 +4378,6 @@ function Menubar({
|
|
|
4386
4378
|
return (
|
|
4387
4379
|
<header
|
|
4388
4380
|
className="st-menubar"
|
|
4389
|
-
role="menubar"
|
|
4390
4381
|
aria-label="Application menubar"
|
|
4391
4382
|
data-testid="menubar"
|
|
4392
4383
|
>
|
|
@@ -4428,7 +4419,7 @@ function Menubar({
|
|
|
4428
4419
|
) : null}
|
|
4429
4420
|
</span>
|
|
4430
4421
|
) : null}
|
|
4431
|
-
<nav className="st-menus" aria-label="Application menus" data-tour="menus">
|
|
4422
|
+
<nav className="st-menus" role="menubar" aria-label="Application menus" data-tour="menus">
|
|
4432
4423
|
{MENU_NAMES.map((name) => {
|
|
4433
4424
|
const key = name.toLowerCase();
|
|
4434
4425
|
const hasDropdown = DROPDOWN_MENUS.includes(key);
|
|
@@ -4794,6 +4785,7 @@ function Viewport({
|
|
|
4794
4785
|
key={`${t.path}#${canvasReloadNonce}`}
|
|
4795
4786
|
ref={(el) => registerIframe(t.path, el)}
|
|
4796
4787
|
src={stableSrc(t.path)}
|
|
4788
|
+
title={`Canvas: ${t.path}`}
|
|
4797
4789
|
className={t.path === activePath ? 'active' : ''}
|
|
4798
4790
|
data-path={t.path}
|
|
4799
4791
|
data-testid={t.path === activePath ? 'canvas-frame' : undefined}
|
|
@@ -6589,6 +6581,25 @@ function GridTracksEditor({ label, tracks, editable, onChange }) {
|
|
|
6589
6581
|
);
|
|
6590
6582
|
}
|
|
6591
6583
|
|
|
6584
|
+
// Writes whose success ends in a `record-edit` post to the canvas. The canvas
|
|
6585
|
+
// asks (`undo-barrier`) before Cmd+Z so it never undoes past an edit whose
|
|
6586
|
+
// record is still on its way (see afterShellRecords in canvas-shell.tsx).
|
|
6587
|
+
const recordableWrites = { pending: 0, waiters: [] };
|
|
6588
|
+
function trackRecordableWrite(promise) {
|
|
6589
|
+
recordableWrites.pending += 1;
|
|
6590
|
+
const settle = () => {
|
|
6591
|
+
recordableWrites.pending -= 1;
|
|
6592
|
+
if (recordableWrites.pending === 0) for (const w of recordableWrites.waiters.splice(0)) w();
|
|
6593
|
+
};
|
|
6594
|
+
promise.then(settle, settle);
|
|
6595
|
+
return promise;
|
|
6596
|
+
}
|
|
6597
|
+
function afterRecordableWrites() {
|
|
6598
|
+
return recordableWrites.pending === 0
|
|
6599
|
+
? Promise.resolve()
|
|
6600
|
+
: new Promise((resolve) => recordableWrites.waiters.push(resolve));
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6592
6603
|
function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoRedo, mode, onSetMode }) {
|
|
6593
6604
|
const editable = !!el.id;
|
|
6594
6605
|
const computed = el.computed || {};
|
|
@@ -6704,13 +6715,15 @@ function CssKnobs({ el, cfg, onOptimistic, onRecordEdit, onReplaceMedia, onUndoR
|
|
|
6704
6715
|
// Serialized so the stack keeps edit order; a failed write records nothing.
|
|
6705
6716
|
const writeChainRef = useRef(Promise.resolve());
|
|
6706
6717
|
const writeAndRecord = (url, payload, key, op, prop, fallbackBefore, after) => {
|
|
6707
|
-
writeChainRef.current =
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6718
|
+
writeChainRef.current = trackRecordableWrite(
|
|
6719
|
+
writeChainRef.current.then(async () => {
|
|
6720
|
+
const j = await post(url, payload, key);
|
|
6721
|
+
if (!j?.ok) return;
|
|
6722
|
+
const before = Object.hasOwn(j, 'previous') ? j.previous : fallbackBefore;
|
|
6723
|
+
if ((before ?? null) === (after ?? null)) return; // nothing changed to undo
|
|
6724
|
+
record(op, prop, before, after);
|
|
6725
|
+
})
|
|
6726
|
+
);
|
|
6714
6727
|
};
|
|
6715
6728
|
// Optimistic preview: nudge the live element so the change shows before the
|
|
6716
6729
|
// edit → HMR reload lands. `value` null = remove (reset path). No-op when the
|
|
@@ -9277,7 +9290,7 @@ function InspectorPanel({
|
|
|
9277
9290
|
// HMR reload re-posts the tree with the new label.
|
|
9278
9291
|
const renameLayer = (node, value) => {
|
|
9279
9292
|
if (!canvasFile || !node?.id) return;
|
|
9280
|
-
fetch('/_api/edit-attr', {
|
|
9293
|
+
trackRecordableWrite(fetch('/_api/edit-attr', {
|
|
9281
9294
|
method: 'POST',
|
|
9282
9295
|
headers: { 'content-type': 'application/json' },
|
|
9283
9296
|
body: JSON.stringify({ canvas: canvasFile, id: node.id, attr: 'data-dc-element', value }),
|
|
@@ -9294,7 +9307,7 @@ function InspectorPanel({
|
|
|
9294
9307
|
after: value,
|
|
9295
9308
|
});
|
|
9296
9309
|
})
|
|
9297
|
-
.catch(() => {});
|
|
9310
|
+
.catch(() => {}));
|
|
9298
9311
|
};
|
|
9299
9312
|
const handleReorder = onReorderLayer
|
|
9300
9313
|
? (dragged, ref, position) => {
|
|
@@ -10779,7 +10792,7 @@ function App() {
|
|
|
10779
10792
|
openPanelExclusive('tree');
|
|
10780
10793
|
setTimeout(() => {
|
|
10781
10794
|
try {
|
|
10782
|
-
document.querySelector('.st-sidebar [
|
|
10795
|
+
document.querySelector('.st-sidebar [data-testid^="canvas-row-"]')?.click();
|
|
10783
10796
|
} catch {}
|
|
10784
10797
|
}, 80);
|
|
10785
10798
|
}
|
|
@@ -12245,6 +12258,7 @@ function App() {
|
|
|
12245
12258
|
// actions (who did what, when), not Git commits; a row previews as `r<rev>`
|
|
12246
12259
|
// through the same version preview. `'legacy'` means "use Git history".
|
|
12247
12260
|
const [projectHistoryOn, setProjectHistoryOn] = useState(false);
|
|
12261
|
+
const [projectHistoryRefresh, setProjectHistoryRefresh] = useState(0);
|
|
12248
12262
|
const loadAcceptedLog = useCallback(async (path) => {
|
|
12249
12263
|
try {
|
|
12250
12264
|
const qs =
|
|
@@ -12273,6 +12287,31 @@ function App() {
|
|
|
12273
12287
|
}
|
|
12274
12288
|
}, []);
|
|
12275
12289
|
|
|
12290
|
+
// An accepted preview must stay on project history even if loading fails:
|
|
12291
|
+
// switching to Git would turn Restore into an unrelated discard operation.
|
|
12292
|
+
const acceptedDiff = /^r\d+$/.test(diffTarget?.beforeSha || '');
|
|
12293
|
+
const loadDiffLog = useCallback(async (path) => {
|
|
12294
|
+
if (!acceptedDiff) return gitLoadLog(path);
|
|
12295
|
+
const entries = await loadAcceptedLog(path);
|
|
12296
|
+
return Array.isArray(entries) ? entries : [];
|
|
12297
|
+
}, [acceptedDiff, gitLoadLog, loadAcceptedLog]);
|
|
12298
|
+
const restoreProjectVersion = useCallback(async (path, revision) => {
|
|
12299
|
+
if (!path || !Number.isSafeInteger(revision) || revision < 0) {
|
|
12300
|
+
return { ok: false, error: 'Choose a saved project version to restore.' };
|
|
12301
|
+
}
|
|
12302
|
+
const r = await fetch('/_api/project/restore', {
|
|
12303
|
+
method: 'POST',
|
|
12304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12305
|
+
body: JSON.stringify({ path, revision }),
|
|
12306
|
+
}).catch(() => null);
|
|
12307
|
+
const result = r ? await r.json().catch(() => null) : null;
|
|
12308
|
+
if (r?.ok && result?.ok) {
|
|
12309
|
+
setProjectHistoryRefresh((version) => version + 1);
|
|
12310
|
+
return result;
|
|
12311
|
+
}
|
|
12312
|
+
return { ok: false, error: result?.error };
|
|
12313
|
+
}, []);
|
|
12314
|
+
|
|
12276
12315
|
// Repo-relative path → M/A/D/U badge for the tree (paths match: both the tree
|
|
12277
12316
|
// and gitStatus use `.design/ui/Foo.tsx`). Keyed off gitStatus so it updates
|
|
12278
12317
|
// live with the WS broadcast.
|
|
@@ -13097,7 +13136,8 @@ function App() {
|
|
|
13097
13136
|
m.dgn === 'select-set' ||
|
|
13098
13137
|
m.dgn === 'clear-select' ||
|
|
13099
13138
|
m.dgn === 'edit-text' ||
|
|
13100
|
-
m.dgn === 'apply-edit'
|
|
13139
|
+
m.dgn === 'apply-edit' ||
|
|
13140
|
+
m.dgn === 'undo-barrier'
|
|
13101
13141
|
) {
|
|
13102
13142
|
const activeWin =
|
|
13103
13143
|
activePath && activePath !== SYSTEM_TAB
|
|
@@ -13175,6 +13215,16 @@ function App() {
|
|
|
13175
13215
|
if (!j.ok) revert(j.error || "this element can't be edited inline");
|
|
13176
13216
|
})
|
|
13177
13217
|
.catch(() => revert('network error'));
|
|
13218
|
+
} else if (m.dgn === 'undo-barrier' && typeof m.requestId === 'string') {
|
|
13219
|
+
// Answer once every write that will post a `record-edit` has settled
|
|
13220
|
+
// (and so posted it — window messages arrive in order), so the canvas's
|
|
13221
|
+
// Cmd+Z sees the edit that is on screen. Read-only; nothing is written.
|
|
13222
|
+
const source = e.source;
|
|
13223
|
+
Promise.all([editApplyChainRef.current.catch(() => {}), afterRecordableWrites()]).then(() => {
|
|
13224
|
+
try {
|
|
13225
|
+
source?.postMessage({ dgn: 'undo-barrier-ok', requestId: m.requestId }, '*');
|
|
13226
|
+
} catch {}
|
|
13227
|
+
});
|
|
13178
13228
|
} else if (m.dgn === 'apply-edit' && m.id && (m.op === 'css' || m.op === 'text' || m.op === 'attr')) {
|
|
13179
13229
|
// Inline-edit undo/redo (DDR-103/104 follow-up). The canvas iframe's
|
|
13180
13230
|
// `edit-source` command can't call the main-origin-only `/_api/edit-*`
|
|
@@ -13661,6 +13711,9 @@ function App() {
|
|
|
13661
13711
|
classes: p.classes,
|
|
13662
13712
|
bounds: p.bounds,
|
|
13663
13713
|
html_excerpt: p.html_excerpt,
|
|
13714
|
+
// #134/#136 anchors — shape-checked server-side (api.ts commentsAdd).
|
|
13715
|
+
annotationId: p.annotationId,
|
|
13716
|
+
world: p.world,
|
|
13664
13717
|
text: txt,
|
|
13665
13718
|
},
|
|
13666
13719
|
});
|
|
@@ -15709,15 +15762,8 @@ function App() {
|
|
|
15709
15762
|
return (cloudManaged ? gitLoadCloudLog : gitLoadLog)(path);
|
|
15710
15763
|
}}
|
|
15711
15764
|
historySource={projectHistoryOn ? 'project' : cloudManaged ? 'cloud' : 'local'}
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
method: 'POST',
|
|
15715
|
-
headers: { 'Content-Type': 'application/json' },
|
|
15716
|
-
body: JSON.stringify({ path: activePath, revision }),
|
|
15717
|
-
}).catch(() => null);
|
|
15718
|
-
const j = r ? await r.json().catch(() => null) : null;
|
|
15719
|
-
return !!j?.ok;
|
|
15720
|
-
}}
|
|
15765
|
+
historyRefresh={`${syncStatus?.appliedRevision ?? 0}:${projectHistoryRefresh}`}
|
|
15766
|
+
onRestoreVersion={(revision) => restoreProjectVersion(activePath, revision).then((r) => !!r.ok)}
|
|
15721
15767
|
// Not through a cell's door: every browser editor proposes under the
|
|
15722
15768
|
// studio's one credential there, so "your own action" is not
|
|
15723
15769
|
// knowable and the hub refuses the route (studio-manifest.mjs).
|
|
@@ -16823,7 +16869,7 @@ function App() {
|
|
|
16823
16869
|
// and was simply unreachable here. Found running S20 against a live
|
|
16824
16870
|
// deployment; asserted in `team-project.e2e.ts` step 4b.
|
|
16825
16871
|
onOpenChanges={
|
|
16826
|
-
gitStatus?.repo || savingIsManaged ? () =>
|
|
16872
|
+
gitStatus?.repo || savingIsManaged ? () => openRightPanel('changes') : undefined
|
|
16827
16873
|
}
|
|
16828
16874
|
version={cfg?.version}
|
|
16829
16875
|
/>
|
|
@@ -16966,10 +17012,12 @@ function App() {
|
|
|
16966
17012
|
<DiffView
|
|
16967
17013
|
target={diffTarget}
|
|
16968
17014
|
cfg={cfg}
|
|
16969
|
-
loadLog={
|
|
17015
|
+
loadLog={loadDiffLog}
|
|
16970
17016
|
onClose={() => setDiffTarget(null)}
|
|
16971
|
-
onRestore={async (file) => {
|
|
16972
|
-
const res =
|
|
17017
|
+
onRestore={async (file, version) => {
|
|
17018
|
+
const res = acceptedDiff
|
|
17019
|
+
? await restoreProjectVersion(file, /^r\d+$/.test(version || '') ? Number(version.slice(1)) : NaN)
|
|
17020
|
+
: await gitDiscard([file]);
|
|
16973
17021
|
if (res?.ok) setDiffTarget(null);
|
|
16974
17022
|
else window.alert(res?.error || 'Could not restore that version. Try again.');
|
|
16975
17023
|
}}
|
|
@@ -65,6 +65,16 @@
|
|
|
65
65
|
opacity: 0.6;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* Detached — the comment's element or annotation is gone from this canvas. The
|
|
69
|
+
comment is kept at its last known place (never deleted by software, #134/#136).
|
|
70
|
+
A hollow, dashed pin: the state reads from the shape, not from colour alone,
|
|
71
|
+
and the number keeps full contrast. The aria-label says "detached" too. */
|
|
72
|
+
.cm-pin[data-detached='true'] {
|
|
73
|
+
background: var(--maude-chrome-bg-1, #faf6ef);
|
|
74
|
+
color: var(--maude-chrome-fg-0, #2a2520);
|
|
75
|
+
border: 1.5px dashed var(--maude-hud-accent, #6f63ef);
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
.cm-pin[data-focused='true'] {
|
|
69
79
|
/* Subtle ring — DS hard-edges discipline: no glow, just a hairline outline. */
|
|
70
80
|
outline: 1px solid var(--maude-chrome-fg-0, #2a2520);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { cloneElement, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const ITEM = '[role="treeitem"]';
|
|
4
|
+
|
|
5
|
+
/** One tree tab stop. Arrow focus does not open files or change selection. */
|
|
6
|
+
export function FileTree({ children, ...props }) {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
useLayoutEffect(() => {
|
|
9
|
+
const tree = ref.current;
|
|
10
|
+
const items = () => Array.from(tree.querySelectorAll(ITEM));
|
|
11
|
+
const focusItem = (item, focus = true) => {
|
|
12
|
+
if (!item) return;
|
|
13
|
+
for (const row of items()) row.tabIndex = row === item ? 0 : -1;
|
|
14
|
+
if (focus) item.focus();
|
|
15
|
+
};
|
|
16
|
+
const repairTabStop = () => {
|
|
17
|
+
const rows = items();
|
|
18
|
+
const current = rows.find((row) => row.tabIndex === 0);
|
|
19
|
+
focusItem(
|
|
20
|
+
current || rows.find((row) => row.getAttribute('aria-selected') === 'true') || rows[0],
|
|
21
|
+
tree.ownerDocument.activeElement === tree
|
|
22
|
+
);
|
|
23
|
+
tree.tabIndex = rows.length ? -1 : 0;
|
|
24
|
+
};
|
|
25
|
+
const onFocus = (event) => {
|
|
26
|
+
const item = event.target.closest?.(ITEM);
|
|
27
|
+
if (item && tree.contains(item)) focusItem(item, false);
|
|
28
|
+
};
|
|
29
|
+
const onKey = (event) => {
|
|
30
|
+
const item = event.target.closest?.(ITEM);
|
|
31
|
+
if (!item || !tree.contains(item) || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
32
|
+
const row = item.querySelector(':scope > .st-row-wrap');
|
|
33
|
+
const primary = row?.querySelector('[data-tree-primary]');
|
|
34
|
+
// Extra buttons keep their native Enter/Space/Tab behavior. Escape
|
|
35
|
+
// returns from a row action to the tree; open menus own their own keys.
|
|
36
|
+
if (event.target !== item && event.target !== primary) {
|
|
37
|
+
if (event.key === 'Escape') {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
focusItem(item);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const rows = items();
|
|
44
|
+
const index = rows.indexOf(item);
|
|
45
|
+
const expanded = item.getAttribute('aria-expanded');
|
|
46
|
+
const toggle = () => item.dispatchEvent(new Event('tree-toggle'));
|
|
47
|
+
switch (event.key) {
|
|
48
|
+
case 'ArrowDown':
|
|
49
|
+
focusItem(rows[Math.min(index + 1, rows.length - 1)]);
|
|
50
|
+
break;
|
|
51
|
+
case 'ArrowUp':
|
|
52
|
+
focusItem(rows[Math.max(index - 1, 0)]);
|
|
53
|
+
break;
|
|
54
|
+
case 'Home':
|
|
55
|
+
focusItem(rows[0]);
|
|
56
|
+
break;
|
|
57
|
+
case 'End':
|
|
58
|
+
focusItem(rows.at(-1));
|
|
59
|
+
break;
|
|
60
|
+
case 'ArrowRight':
|
|
61
|
+
if (expanded === 'false') toggle();
|
|
62
|
+
else if (expanded === 'true')
|
|
63
|
+
focusItem(item.querySelector(':scope > [role="group"] ' + ITEM));
|
|
64
|
+
break;
|
|
65
|
+
case 'ArrowLeft':
|
|
66
|
+
if (expanded === 'true') toggle();
|
|
67
|
+
else focusItem(item.parentElement?.closest(ITEM));
|
|
68
|
+
break;
|
|
69
|
+
case 'Enter':
|
|
70
|
+
case ' ':
|
|
71
|
+
if (event.target !== item) return;
|
|
72
|
+
if (item.getAttribute('aria-disabled') !== 'true') primary?.click();
|
|
73
|
+
break;
|
|
74
|
+
default: {
|
|
75
|
+
if (event.key.length !== 1 || event.shiftKey) return;
|
|
76
|
+
const ordered = [...rows.slice(index + 1), ...rows.slice(0, index + 1)];
|
|
77
|
+
focusItem(
|
|
78
|
+
ordered.find((candidate) =>
|
|
79
|
+
candidate
|
|
80
|
+
.getAttribute('aria-label')
|
|
81
|
+
?.toLocaleLowerCase()
|
|
82
|
+
.startsWith(event.key.toLocaleLowerCase())
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
};
|
|
90
|
+
repairTabStop();
|
|
91
|
+
const observer = new MutationObserver(repairTabStop);
|
|
92
|
+
observer.observe(tree, { childList: true, subtree: true });
|
|
93
|
+
tree.addEventListener('focusin', onFocus);
|
|
94
|
+
tree.addEventListener('keydown', onKey);
|
|
95
|
+
return () => {
|
|
96
|
+
observer.disconnect();
|
|
97
|
+
tree.removeEventListener('focusin', onFocus);
|
|
98
|
+
tree.removeEventListener('keydown', onKey);
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
return (
|
|
102
|
+
<div {...props} ref={ref} className="st-tree" role="tree">
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Row actions belong to the item; descendants belong to its named group.
|
|
109
|
+
* Native buttons remain the activation targets, never nested in a button. */
|
|
110
|
+
export function FileTreeItem({
|
|
111
|
+
label,
|
|
112
|
+
row,
|
|
113
|
+
actions,
|
|
114
|
+
expanded,
|
|
115
|
+
selected,
|
|
116
|
+
disabled,
|
|
117
|
+
busy,
|
|
118
|
+
onToggle,
|
|
119
|
+
children,
|
|
120
|
+
className = '',
|
|
121
|
+
}) {
|
|
122
|
+
const ref = useRef(null);
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
const item = ref.current;
|
|
125
|
+
const toggle = () => onToggle?.();
|
|
126
|
+
item.addEventListener('tree-toggle', toggle);
|
|
127
|
+
return () => item.removeEventListener('tree-toggle', toggle);
|
|
128
|
+
}, [onToggle]);
|
|
129
|
+
const primary =
|
|
130
|
+
row.type === 'button'
|
|
131
|
+
? cloneElement(row, {
|
|
132
|
+
role: undefined,
|
|
133
|
+
'aria-selected': undefined,
|
|
134
|
+
tabIndex: -1,
|
|
135
|
+
'data-tree-primary': '',
|
|
136
|
+
})
|
|
137
|
+
: row;
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
ref={ref}
|
|
141
|
+
className={`st-tree-item ${className}`}
|
|
142
|
+
role="treeitem"
|
|
143
|
+
tabIndex={-1}
|
|
144
|
+
aria-label={label}
|
|
145
|
+
aria-expanded={expanded}
|
|
146
|
+
aria-selected={selected}
|
|
147
|
+
aria-disabled={disabled || undefined}
|
|
148
|
+
aria-busy={busy || undefined}
|
|
149
|
+
>
|
|
150
|
+
<div className="st-row-wrap">
|
|
151
|
+
{primary}
|
|
152
|
+
{actions}
|
|
153
|
+
</div>
|
|
154
|
+
{children && <div role="group">{children}</div>}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|