@1agh/maude 0.24.0 → 0.25.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/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
// Renders: file tree, tabs, viewport (iframes), status bar, design-system view, comments.
|
|
4
4
|
// Universal — no project tokens needed; styling lives in client/styles/.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
import { createRoot } from 'react-dom/client';
|
|
8
8
|
|
|
9
|
+
// Trusted tool→cursor resolver (shares the single TOOL_CURSORS source with the
|
|
10
|
+
// canvas runtime). canvas-cursors.ts is dependency-free (a type-only Tool
|
|
11
|
+
// import that Bun erases), so this pulls only string constants into the client
|
|
12
|
+
// bundle — no React, no input-router. See the tool-cursor handler below.
|
|
13
|
+
import { resolveToolCursor } from '../canvas-cursors.ts';
|
|
14
|
+
|
|
9
15
|
const SYSTEM_TAB = '__system__';
|
|
10
16
|
const THEME_STORE = 'mdcc-theme';
|
|
11
17
|
const SHOW_HIDDEN_STORE = 'mdcc-show-hidden';
|
|
@@ -41,7 +47,9 @@ function readJsonStore(key, fallback) {
|
|
|
41
47
|
try {
|
|
42
48
|
const v = localStorage.getItem(key);
|
|
43
49
|
return v ? JSON.parse(v) : fallback;
|
|
44
|
-
} catch {
|
|
50
|
+
} catch {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// Section default-open: working sections (project + non-DS canvas groups)
|
|
@@ -98,8 +106,8 @@ function canvasUrl(p, cfg, opts) {
|
|
|
98
106
|
// tokens file. The top-level cfg.tokensCssRel is the legacy default
|
|
99
107
|
// (`system/colors_and_type.css`) and usually doesn't exist post-bootstrap.
|
|
100
108
|
const tokens = specMatch
|
|
101
|
-
?
|
|
102
|
-
:
|
|
109
|
+
? specDsEntry?.tokensCssRel || `system/${specMatch[1]}/colors_and_type.css`
|
|
110
|
+
: ds0?.tokensCssRel || cfg?.tokensCssRel;
|
|
103
111
|
if (tokens) params.set('tokens', tokens);
|
|
104
112
|
if (cfg?.componentsCssRel) params.set('components', cfg.componentsCssRel);
|
|
105
113
|
if (specMatch) {
|
|
@@ -192,7 +200,9 @@ function groupBySidecar(files) {
|
|
|
192
200
|
function buildTree(paths, stripPrefix) {
|
|
193
201
|
const root = {};
|
|
194
202
|
for (const p of paths) {
|
|
195
|
-
const stripped = p.startsWith(stripPrefix)
|
|
203
|
+
const stripped = p.startsWith(stripPrefix)
|
|
204
|
+
? p.slice(stripPrefix.length).replace(/^\/+/, '')
|
|
205
|
+
: p;
|
|
196
206
|
const parts = stripped.split('/');
|
|
197
207
|
let node = root;
|
|
198
208
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -215,22 +225,28 @@ function filterTree(node, query) {
|
|
|
215
225
|
const q = query.toLowerCase();
|
|
216
226
|
const out = {};
|
|
217
227
|
let any = false;
|
|
218
|
-
const dirs = Object.keys(node).filter(k => k !== '_files');
|
|
228
|
+
const dirs = Object.keys(node).filter((k) => k !== '_files');
|
|
219
229
|
for (const d of dirs) {
|
|
220
230
|
const filtered = filterTree(node[d], query);
|
|
221
|
-
if (filtered) {
|
|
231
|
+
if (filtered) {
|
|
232
|
+
out[d] = filtered;
|
|
233
|
+
any = true;
|
|
234
|
+
}
|
|
222
235
|
}
|
|
223
236
|
if (node._files) {
|
|
224
|
-
const files = node._files.filter(
|
|
225
|
-
f.name.toLowerCase().includes(q) || f.path.toLowerCase().includes(q)
|
|
237
|
+
const files = node._files.filter(
|
|
238
|
+
(f) => f.name.toLowerCase().includes(q) || f.path.toLowerCase().includes(q)
|
|
226
239
|
);
|
|
227
|
-
if (files.length) {
|
|
240
|
+
if (files.length) {
|
|
241
|
+
out._files = files;
|
|
242
|
+
any = true;
|
|
243
|
+
}
|
|
228
244
|
}
|
|
229
245
|
return any ? out : null;
|
|
230
246
|
}
|
|
231
247
|
|
|
232
248
|
function openCount(comments) {
|
|
233
|
-
return (comments || []).filter(c => c.status !== 'resolved').length;
|
|
249
|
+
return (comments || []).filter((c) => c.status !== 'resolved').length;
|
|
234
250
|
}
|
|
235
251
|
|
|
236
252
|
function timeAgo(iso) {
|
|
@@ -249,7 +265,9 @@ function timeAgo(iso) {
|
|
|
249
265
|
}
|
|
250
266
|
|
|
251
267
|
function totalCounts(commentsByFile) {
|
|
252
|
-
let all = 0,
|
|
268
|
+
let all = 0,
|
|
269
|
+
open = 0,
|
|
270
|
+
resolved = 0;
|
|
253
271
|
for (const list of Object.values(commentsByFile || {})) {
|
|
254
272
|
for (const c of list || []) {
|
|
255
273
|
all++;
|
|
@@ -264,7 +282,17 @@ function totalCounts(commentsByFile) {
|
|
|
264
282
|
|
|
265
283
|
function Icon({ d, size = 14, color }) {
|
|
266
284
|
return (
|
|
267
|
-
<svg
|
|
285
|
+
<svg
|
|
286
|
+
width={size}
|
|
287
|
+
height={size}
|
|
288
|
+
viewBox="0 0 24 24"
|
|
289
|
+
fill="none"
|
|
290
|
+
stroke={color || 'currentColor'}
|
|
291
|
+
strokeWidth="1.5"
|
|
292
|
+
strokeLinecap="round"
|
|
293
|
+
strokeLinejoin="round"
|
|
294
|
+
style={{ flex: 'none' }}
|
|
295
|
+
>
|
|
268
296
|
<path d={d} />
|
|
269
297
|
</svg>
|
|
270
298
|
);
|
|
@@ -291,9 +319,11 @@ function DirRow({ name, depth, defaultOpen, children }) {
|
|
|
291
319
|
tabIndex={-1}
|
|
292
320
|
className="tp-row dir"
|
|
293
321
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
294
|
-
onClick={() => setOpen(v => !v)}
|
|
322
|
+
onClick={() => setOpen((v) => !v)}
|
|
295
323
|
>
|
|
296
|
-
<span className="glyph" aria-hidden="true">
|
|
324
|
+
<span className="glyph" aria-hidden="true">
|
|
325
|
+
{open ? '▾' : '▸'}
|
|
326
|
+
</span>
|
|
297
327
|
<span className="name">{name}</span>
|
|
298
328
|
</button>
|
|
299
329
|
{open && children}
|
|
@@ -322,7 +352,9 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
322
352
|
aria-label={open ? 'Collapse design system' : 'Expand design system'}
|
|
323
353
|
title={open ? 'Collapse' : 'Expand'}
|
|
324
354
|
>
|
|
325
|
-
<span className="glyph" aria-hidden="true">
|
|
355
|
+
<span className="glyph" aria-hidden="true">
|
|
356
|
+
{open ? '▾' : '▸'}
|
|
357
|
+
</span>
|
|
326
358
|
</button>
|
|
327
359
|
<button
|
|
328
360
|
type="button"
|
|
@@ -354,19 +386,38 @@ function FileRow({ file, activePath, onOpen, openCount: oc, depth, kind, sidecar
|
|
|
354
386
|
aria-selected={isSel}
|
|
355
387
|
aria-disabled={inert ? 'true' : undefined}
|
|
356
388
|
tabIndex={isSel ? 0 : -1}
|
|
357
|
-
className={
|
|
389
|
+
className={
|
|
390
|
+
'tp-row' +
|
|
391
|
+
(isSel ? ' sel' : '') +
|
|
392
|
+
(kind === 'runtime' ? ' muted' : '') +
|
|
393
|
+
(sidecar ? ' sidecar' : '')
|
|
394
|
+
}
|
|
358
395
|
style={{ paddingLeft: TREE_INDENT_BASE + depth * TREE_INDENT_STEP + 'px' }}
|
|
359
|
-
title={file.path + (oc ? ` — ${oc} open` :
|
|
360
|
-
onClick={() => {
|
|
396
|
+
title={file.path + (oc ? ` — ${oc} open` : inert ? ' (file index only)' : '')}
|
|
397
|
+
onClick={() => {
|
|
398
|
+
if (!inert) onOpen(file.path);
|
|
399
|
+
}}
|
|
361
400
|
>
|
|
362
|
-
<span className="glyph" aria-hidden="true">
|
|
401
|
+
<span className="glyph" aria-hidden="true">
|
|
402
|
+
{isSel ? '▸' : '·'}
|
|
403
|
+
</span>
|
|
363
404
|
<span className="name">{label}</span>
|
|
364
405
|
{oc > 0 && <span className="badge">{oc}</span>}
|
|
365
406
|
</button>
|
|
366
407
|
);
|
|
367
408
|
}
|
|
368
409
|
|
|
369
|
-
function CanvasRow({
|
|
410
|
+
function CanvasRow({
|
|
411
|
+
primary,
|
|
412
|
+
sidecars,
|
|
413
|
+
depth,
|
|
414
|
+
kind,
|
|
415
|
+
activePath,
|
|
416
|
+
onOpen,
|
|
417
|
+
openCount: oc,
|
|
418
|
+
showHidden,
|
|
419
|
+
forceOpen,
|
|
420
|
+
}) {
|
|
370
421
|
const hasSidecars = sidecars.length > 0;
|
|
371
422
|
const [openState, setOpenState] = useState(false);
|
|
372
423
|
// Sidecars are only revealed when the user opts in via `showHidden` — the
|
|
@@ -420,24 +471,39 @@ function CanvasRow({ primary, sidecars, depth, kind, activePath, onOpen, openCou
|
|
|
420
471
|
<span className="name">{displayName(primary.name)}</span>
|
|
421
472
|
{oc > 0 && <span className="badge">{oc}</span>}
|
|
422
473
|
</button>
|
|
423
|
-
{open &&
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
474
|
+
{open &&
|
|
475
|
+
sidecars.map((sc) => (
|
|
476
|
+
<FileRow
|
|
477
|
+
key={sc.path}
|
|
478
|
+
file={sc}
|
|
479
|
+
activePath={activePath}
|
|
480
|
+
onOpen={onOpen}
|
|
481
|
+
openCount={0}
|
|
482
|
+
depth={depth + 1}
|
|
483
|
+
kind={kind}
|
|
484
|
+
sidecar
|
|
485
|
+
/>
|
|
486
|
+
))}
|
|
435
487
|
</Fragment>
|
|
436
488
|
);
|
|
437
489
|
}
|
|
438
490
|
|
|
439
|
-
function Tree({
|
|
440
|
-
|
|
491
|
+
function Tree({
|
|
492
|
+
node,
|
|
493
|
+
activePath,
|
|
494
|
+
onOpen,
|
|
495
|
+
commentsByFile,
|
|
496
|
+
depth = 1,
|
|
497
|
+
kind,
|
|
498
|
+
showHidden,
|
|
499
|
+
search,
|
|
500
|
+
dsFolders,
|
|
501
|
+
activeDsName,
|
|
502
|
+
onOpenSystem,
|
|
503
|
+
}) {
|
|
504
|
+
const dirs = Object.keys(node)
|
|
505
|
+
.filter((k) => k !== '_files')
|
|
506
|
+
.sort();
|
|
441
507
|
const files = node._files || [];
|
|
442
508
|
// VS Code-style sidecar grouping. Canvas (`.tsx`/`.html`) becomes the primary
|
|
443
509
|
// row; same-basename non-canvas files (`.meta.json`, `.css`, …) collapse
|
|
@@ -456,10 +522,12 @@ function Tree({ node, activePath, onOpen, commentsByFile, depth = 1, kind, showH
|
|
|
456
522
|
return (
|
|
457
523
|
<Fragment>
|
|
458
524
|
{canvases.map((entry) => {
|
|
459
|
-
const forceOpen =
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
525
|
+
const forceOpen =
|
|
526
|
+
hasSearch &&
|
|
527
|
+
entry.sidecars.some((sc) => {
|
|
528
|
+
const q = search.toLowerCase();
|
|
529
|
+
return sc.name.toLowerCase().includes(q) || sc.path.toLowerCase().includes(q);
|
|
530
|
+
});
|
|
463
531
|
return (
|
|
464
532
|
<CanvasRow
|
|
465
533
|
key={entry.primary.path}
|
|
@@ -475,18 +543,19 @@ function Tree({ node, activePath, onOpen, commentsByFile, depth = 1, kind, showH
|
|
|
475
543
|
/>
|
|
476
544
|
);
|
|
477
545
|
})}
|
|
478
|
-
{showHidden &&
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
546
|
+
{showHidden &&
|
|
547
|
+
orphans.map((entry) => (
|
|
548
|
+
<FileRow
|
|
549
|
+
key={entry.primary.path}
|
|
550
|
+
file={entry.primary}
|
|
551
|
+
activePath={activePath}
|
|
552
|
+
onOpen={onOpen}
|
|
553
|
+
openCount={openCount(commentsByFile[entry.primary.path])}
|
|
554
|
+
depth={depth}
|
|
555
|
+
kind={kind}
|
|
556
|
+
/>
|
|
557
|
+
))}
|
|
558
|
+
{dirs.map((d) => {
|
|
490
559
|
const dsMatch = dsFolderByName?.get(d);
|
|
491
560
|
const childTree = (
|
|
492
561
|
<Tree
|
|
@@ -531,12 +600,12 @@ function Tree({ node, activePath, onOpen, commentsByFile, depth = 1, kind, showH
|
|
|
531
600
|
// project / DS identity; the mock keeps these tight (1 line). Labels are
|
|
532
601
|
// keyed by the server-provided `kind` (PROJECT / DS / UI / RUNTIME).
|
|
533
602
|
const SECTION_META = {
|
|
534
|
-
project:
|
|
603
|
+
project: { title: 'PROJECT', pillFromCount: false },
|
|
535
604
|
// Design-system group: pill shows the number of DSes (one row per DS folder
|
|
536
605
|
// inside). Computed in Sidebar from `g.dsFolders.length`.
|
|
537
|
-
ds:
|
|
538
|
-
canvas:
|
|
539
|
-
runtime:
|
|
606
|
+
ds: { title: 'DESIGN SYSTEM', pillFromDsCount: true },
|
|
607
|
+
canvas: { title: 'UI CANVASES', pillFromCount: true },
|
|
608
|
+
runtime: { title: 'RUNTIME · GITIGNORED', pillFromCount: true },
|
|
540
609
|
};
|
|
541
610
|
|
|
542
611
|
function sectionMetaFor(g) {
|
|
@@ -544,14 +613,27 @@ function sectionMetaFor(g) {
|
|
|
544
613
|
if (g.kind === 'runtime') return SECTION_META.runtime;
|
|
545
614
|
// canvas-kind groups: "Design system" → ds, anything else → canvas label
|
|
546
615
|
if (g.label === 'Design system') return SECTION_META.ds;
|
|
547
|
-
if (g.label === 'UI kit')
|
|
616
|
+
if (g.label === 'UI kit') return SECTION_META.canvas;
|
|
548
617
|
return { title: g.label.toUpperCase(), pillFromCount: true };
|
|
549
618
|
}
|
|
550
619
|
|
|
551
|
-
function Sidebar({
|
|
620
|
+
function Sidebar({
|
|
621
|
+
groups,
|
|
622
|
+
activePath,
|
|
623
|
+
activeDsName,
|
|
624
|
+
onOpen,
|
|
625
|
+
onOpenSystem,
|
|
626
|
+
wsConnected,
|
|
627
|
+
search,
|
|
628
|
+
setSearch,
|
|
629
|
+
commentsByFile,
|
|
630
|
+
showHidden,
|
|
631
|
+
sectionsExpanded,
|
|
632
|
+
onToggleSection,
|
|
633
|
+
}) {
|
|
552
634
|
const filteredGroups = useMemo(() => {
|
|
553
635
|
if (!search) return groups;
|
|
554
|
-
return groups.map(g => ({ ...g, tree: filterTree(g.tree, search), filtered: !!search }));
|
|
636
|
+
return groups.map((g) => ({ ...g, tree: filterTree(g.tree, search), filtered: !!search }));
|
|
555
637
|
}, [groups, search]);
|
|
556
638
|
|
|
557
639
|
// Mock uses `42 / 42` — total openable canvases, not every listed file.
|
|
@@ -564,7 +646,8 @@ function Sidebar({ groups, activePath, activeDsName, onOpen, onOpenSystem, wsCon
|
|
|
564
646
|
}, [groups]);
|
|
565
647
|
const htmlShown = useMemo(() => {
|
|
566
648
|
let total = 0;
|
|
567
|
-
for (const g of filteredGroups)
|
|
649
|
+
for (const g of filteredGroups)
|
|
650
|
+
for (const p of g.paths || []) if (CANVAS_EXT_RE.test(p)) total++;
|
|
568
651
|
return total;
|
|
569
652
|
}, [filteredGroups]);
|
|
570
653
|
|
|
@@ -584,18 +667,27 @@ function Sidebar({ groups, activePath, activeDsName, onOpen, onOpenSystem, wsCon
|
|
|
584
667
|
type="search"
|
|
585
668
|
placeholder="filter (⌘F)"
|
|
586
669
|
value={search}
|
|
587
|
-
onChange={e => setSearch(e.target.value)}
|
|
670
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
588
671
|
aria-label="Filter files"
|
|
589
672
|
/>
|
|
590
673
|
{search ? (
|
|
591
|
-
<button
|
|
674
|
+
<button
|
|
675
|
+
className="search-clear"
|
|
676
|
+
onClick={() => setSearch('')}
|
|
677
|
+
title="Clear (Esc)"
|
|
678
|
+
aria-label="Clear search"
|
|
679
|
+
>
|
|
680
|
+
×
|
|
681
|
+
</button>
|
|
592
682
|
) : (
|
|
593
|
-
<span className="search-kbd" aria-hidden="true"
|
|
683
|
+
<span className="search-kbd" aria-hidden="true">
|
|
684
|
+
/
|
|
685
|
+
</span>
|
|
594
686
|
)}
|
|
595
687
|
</div>
|
|
596
688
|
|
|
597
689
|
<div className="tree-panel-body" role="tree" aria-label="Project file tree">
|
|
598
|
-
{filteredGroups.map(g => {
|
|
690
|
+
{filteredGroups.map((g) => {
|
|
599
691
|
// Hide gitignored runtime / orphan-only project sections by default.
|
|
600
692
|
// Active search overrides — if the user typed a query, they want hits
|
|
601
693
|
// wherever they live.
|
|
@@ -604,9 +696,10 @@ function Sidebar({ groups, activePath, activeDsName, onOpen, onOpenSystem, wsCon
|
|
|
604
696
|
// Counter pill counts canvases only — sidecars + orphans inflate the
|
|
605
697
|
// raw `paths.length` and the FILES header already filters this way.
|
|
606
698
|
const canvasCount = (g.paths || []).filter((p) => CANVAS_EXT_RE.test(p)).length;
|
|
607
|
-
const pill =
|
|
608
|
-
|
|
609
|
-
|
|
699
|
+
const pill =
|
|
700
|
+
meta.pill ||
|
|
701
|
+
(meta.pillFromDsCount ? String(g.dsFolders?.length || 0) : null) ||
|
|
702
|
+
(meta.pillFromCount ? String(canvasCount || g.paths?.length || 0) : null);
|
|
610
703
|
const hasItems = g.tree && Object.keys(g.tree).length > 0;
|
|
611
704
|
const isDs = g.label === 'Design system';
|
|
612
705
|
const isProject = g.kind === 'project';
|
|
@@ -628,29 +721,30 @@ function Sidebar({ groups, activePath, activeDsName, onOpen, onOpenSystem, wsCon
|
|
|
628
721
|
aria-expanded={sectionOpen}
|
|
629
722
|
title={sectionOpen ? 'Collapse section' : 'Expand section'}
|
|
630
723
|
>
|
|
631
|
-
<span className="chev" aria-hidden="true">
|
|
724
|
+
<span className="chev" aria-hidden="true">
|
|
725
|
+
{chev}
|
|
726
|
+
</span>
|
|
632
727
|
<span className="section-label">{meta.title}</span>
|
|
633
728
|
{pill && <span className="pill">{pill}</span>}
|
|
634
729
|
</button>
|
|
635
|
-
{sectionOpen &&
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
{search ? 'No matches.' : 'Empty.'}
|
|
652
|
-
|
|
653
|
-
))}
|
|
730
|
+
{sectionOpen &&
|
|
731
|
+
(hasItems ? (
|
|
732
|
+
<Tree
|
|
733
|
+
node={g.tree}
|
|
734
|
+
activePath={activePath}
|
|
735
|
+
onOpen={onOpen}
|
|
736
|
+
commentsByFile={commentsByFile}
|
|
737
|
+
depth={1}
|
|
738
|
+
kind={g.kind}
|
|
739
|
+
showHidden={showHidden}
|
|
740
|
+
search={search}
|
|
741
|
+
dsFolders={g.dsFolders}
|
|
742
|
+
activeDsName={activeDsName}
|
|
743
|
+
onOpenSystem={isDs ? onOpenSystem : undefined}
|
|
744
|
+
/>
|
|
745
|
+
) : (
|
|
746
|
+
<div className="tp-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
747
|
+
))}
|
|
654
748
|
</Fragment>
|
|
655
749
|
);
|
|
656
750
|
})}
|
|
@@ -664,7 +758,9 @@ function Sidebar({ groups, activePath, activeDsName, onOpen, onOpenSystem, wsCon
|
|
|
664
758
|
function HelpModal({ open, onClose }) {
|
|
665
759
|
useEffect(() => {
|
|
666
760
|
if (!open) return;
|
|
667
|
-
function onKey(e) {
|
|
761
|
+
function onKey(e) {
|
|
762
|
+
if (e.key === 'Escape') onClose();
|
|
763
|
+
}
|
|
668
764
|
window.addEventListener('keydown', onKey);
|
|
669
765
|
return () => window.removeEventListener('keydown', onKey);
|
|
670
766
|
}, [open, onClose]);
|
|
@@ -673,112 +769,286 @@ function HelpModal({ open, onClose }) {
|
|
|
673
769
|
<div
|
|
674
770
|
className="help-modal-backdrop"
|
|
675
771
|
role="presentation"
|
|
676
|
-
onMouseDown={e => {
|
|
772
|
+
onMouseDown={(e) => {
|
|
773
|
+
if (e.target === e.currentTarget) onClose();
|
|
774
|
+
}}
|
|
677
775
|
>
|
|
678
|
-
<div
|
|
776
|
+
<div
|
|
777
|
+
className="help-modal"
|
|
778
|
+
role="dialog"
|
|
779
|
+
aria-modal="true"
|
|
780
|
+
aria-labelledby="help-modal-title"
|
|
781
|
+
>
|
|
679
782
|
<header className="help-modal-hd">
|
|
680
|
-
<span className="title" id="help-modal-title">
|
|
783
|
+
<span className="title" id="help-modal-title">
|
|
784
|
+
Help · shortcuts & commands
|
|
785
|
+
</span>
|
|
681
786
|
<span className="sku">MDCC-DEV-SRV / v{MDCC_VERSION}</span>
|
|
682
|
-
<button
|
|
787
|
+
<button
|
|
788
|
+
type="button"
|
|
789
|
+
className="help-modal-close"
|
|
790
|
+
aria-label="Close (Esc)"
|
|
791
|
+
onClick={onClose}
|
|
792
|
+
>
|
|
793
|
+
×
|
|
794
|
+
</button>
|
|
683
795
|
</header>
|
|
684
796
|
<div className="help-modal-body">
|
|
685
797
|
<details open>
|
|
686
798
|
<summary>Canvas selection & tools</summary>
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
799
|
+
<ul>
|
|
800
|
+
<li>
|
|
801
|
+
<kbd>V</kbd> <span>move tool — Cmd+click to select, Cmd+Shift to multi</span>
|
|
802
|
+
</li>
|
|
803
|
+
<li>
|
|
804
|
+
<kbd>H</kbd> <span>hand tool — bare drag pans (no Space needed)</span>
|
|
805
|
+
</li>
|
|
806
|
+
<li>
|
|
807
|
+
<kbd>C</kbd> <span>comment tool — hover paints, click drops a pin</span>
|
|
808
|
+
</li>
|
|
809
|
+
<li>
|
|
810
|
+
<kbd>⌘</kbd> + hover <span>preview deepest element under cursor</span>
|
|
811
|
+
</li>
|
|
812
|
+
<li>
|
|
813
|
+
<kbd>⌘</kbd> + click <span>select that element (replace)</span>
|
|
814
|
+
</li>
|
|
815
|
+
<li>
|
|
816
|
+
<kbd>⌘⇧</kbd> + click <span>add deepest to selection (multi)</span>
|
|
817
|
+
</li>
|
|
818
|
+
<li>
|
|
819
|
+
right-click <span>context menu (Copy CSS / Fit / Reset...)</span>
|
|
820
|
+
</li>
|
|
821
|
+
<li>
|
|
822
|
+
<kbd>Esc</kbd> in canvas <span>clear selection + close menu</span>
|
|
823
|
+
</li>
|
|
824
|
+
</ul>
|
|
825
|
+
</details>
|
|
826
|
+
<details>
|
|
827
|
+
<summary>Annotation tools</summary>
|
|
828
|
+
<ul>
|
|
829
|
+
<li>
|
|
830
|
+
<kbd>B</kbd> <span>pen — freehand stroke</span>
|
|
831
|
+
</li>
|
|
832
|
+
<li>
|
|
833
|
+
<kbd>R</kbd> <span>rectangle — drag to define corners</span>
|
|
834
|
+
</li>
|
|
835
|
+
<li>
|
|
836
|
+
<kbd>O</kbd> <span>ellipse — drag from center outward</span>
|
|
837
|
+
</li>
|
|
838
|
+
<li>
|
|
839
|
+
<kbd>A</kbd> <span>arrow — drag tail → tip</span>
|
|
840
|
+
</li>
|
|
841
|
+
<li>
|
|
842
|
+
<kbd>E</kbd> <span>eraser — click or drag over strokes to remove</span>
|
|
843
|
+
</li>
|
|
844
|
+
<li>
|
|
845
|
+
<kbd>V</kbd> + click stroke <span>select annotation (Shift+click to multi)</span>
|
|
846
|
+
</li>
|
|
847
|
+
<li>
|
|
848
|
+
<kbd>V</kbd> + drag empty <span>marquee-select strokes that overlap</span>
|
|
849
|
+
</li>
|
|
850
|
+
<li>
|
|
851
|
+
double-click rect/ellipse <span>add text inside the shape</span>
|
|
852
|
+
</li>
|
|
853
|
+
<li>
|
|
854
|
+
arrow keys <span>nudge selected annotation 1 unit (Shift = 10)</span>
|
|
855
|
+
</li>
|
|
856
|
+
<li>
|
|
857
|
+
<kbd>Backspace</kbd> <span>delete selected annotations</span>
|
|
858
|
+
</li>
|
|
859
|
+
<li>
|
|
860
|
+
<kbd>⇧P</kbd> <span>presentation — hide annotations for clean screenshot</span>
|
|
861
|
+
</li>
|
|
862
|
+
</ul>
|
|
863
|
+
</details>
|
|
864
|
+
<details>
|
|
865
|
+
<summary>Tabs & canvas</summary>
|
|
866
|
+
<ul>
|
|
867
|
+
<li>
|
|
868
|
+
click in tree <span>open tab</span>
|
|
869
|
+
</li>
|
|
870
|
+
<li>
|
|
871
|
+
<kbd>×</kbd> on tab <span>close tab</span>
|
|
872
|
+
</li>
|
|
873
|
+
<li>
|
|
874
|
+
<kbd>⌘R</kbd> <span>reload iframe</span>
|
|
875
|
+
</li>
|
|
876
|
+
<li>
|
|
877
|
+
<kbd>/</kbd> <span>focus search</span>
|
|
878
|
+
</li>
|
|
879
|
+
<li>
|
|
880
|
+
<kbd>⌘⇧M</kbd> <span>toggle comments panel</span>
|
|
881
|
+
</li>
|
|
882
|
+
</ul>
|
|
883
|
+
</details>
|
|
884
|
+
<details>
|
|
885
|
+
<summary>Slash commands</summary>
|
|
886
|
+
<ul className="cmds">
|
|
887
|
+
<li>
|
|
888
|
+
<code>
|
|
889
|
+
/design:edit "<i>feedback</i>"
|
|
890
|
+
</code>
|
|
891
|
+
<span>edit + 4-iter multi-axis loop</span>
|
|
892
|
+
</li>
|
|
893
|
+
<li>
|
|
894
|
+
<code>
|
|
895
|
+
/design:edit "<i>…</i>" --perfect
|
|
896
|
+
</code>
|
|
897
|
+
<span>8-iter polish (4.5/5 aspiration)</span>
|
|
898
|
+
</li>
|
|
899
|
+
<li>
|
|
900
|
+
<code>
|
|
901
|
+
/design:edit "<i>…</i>" --no-critic
|
|
902
|
+
</code>
|
|
903
|
+
<span>raw edit, skip loop</span>
|
|
904
|
+
</li>
|
|
905
|
+
<li>
|
|
906
|
+
<code>
|
|
907
|
+
/design:edit "<i>…</i>" --opt-out=<i>scope</i>
|
|
908
|
+
</code>
|
|
909
|
+
<span>override DS scope (palette/aesthetic/full)</span>
|
|
910
|
+
</li>
|
|
911
|
+
<li>
|
|
912
|
+
<code>
|
|
913
|
+
/design:new "<i>Name</i>" "<i>brief</i>"
|
|
914
|
+
</code>
|
|
915
|
+
<span>scaffold canvas</span>
|
|
916
|
+
</li>
|
|
917
|
+
<li>
|
|
918
|
+
<code>
|
|
919
|
+
/design:new "<i>…</i>" --opt-out=aesthetic
|
|
920
|
+
</code>
|
|
921
|
+
<span>scaffold off-system canvas (gradients/radii/type free)</span>
|
|
922
|
+
</li>
|
|
923
|
+
<li>
|
|
924
|
+
<code>/design:critic</code>
|
|
925
|
+
<span>review panel (routed)</span>
|
|
926
|
+
</li>
|
|
927
|
+
<li>
|
|
928
|
+
<code>/design:critic --all</code>
|
|
929
|
+
<span>10-critic sweep</span>
|
|
930
|
+
</li>
|
|
931
|
+
<li>
|
|
932
|
+
<code>/design:critic --agent signature-moment-critic</code>
|
|
933
|
+
<span>aspiration axis only</span>
|
|
934
|
+
</li>
|
|
935
|
+
<li>
|
|
936
|
+
<code>/design:rollback</code>
|
|
937
|
+
<span>undo last edit</span>
|
|
938
|
+
</li>
|
|
939
|
+
<li>
|
|
940
|
+
<code>/design:screenshot</code>
|
|
941
|
+
<span>capture canvas</span>
|
|
942
|
+
</li>
|
|
943
|
+
<li>
|
|
944
|
+
<code>/design:setup-docs</code>
|
|
945
|
+
<span>refresh README + INDEX</span>
|
|
946
|
+
</li>
|
|
947
|
+
<li>
|
|
948
|
+
<code>/design:handoff</code>
|
|
949
|
+
<span>migrate to apps/</span>
|
|
950
|
+
</li>
|
|
951
|
+
</ul>
|
|
952
|
+
</details>
|
|
953
|
+
<details>
|
|
954
|
+
<summary>Opt-out scope</summary>
|
|
955
|
+
<ul>
|
|
956
|
+
<li>
|
|
957
|
+
<strong>palette</strong>{' '}
|
|
958
|
+
<span>
|
|
959
|
+
default — tokens + rootClass kept; local namespace overrides colors only. DS
|
|
960
|
+
aesthetic still enforced.
|
|
961
|
+
</span>
|
|
962
|
+
</li>
|
|
963
|
+
<li>
|
|
964
|
+
<strong>aesthetic</strong>{' '}
|
|
965
|
+
<span>
|
|
966
|
+
palette + gradients/off-ladder radii/alt type/decorative SVG flags allowed.
|
|
967
|
+
</span>
|
|
968
|
+
</li>
|
|
969
|
+
<li>
|
|
970
|
+
<strong>full</strong>{' '}
|
|
971
|
+
<span>DS treated as advisory. Type/radii/aesthetic up to canvas.</span>
|
|
972
|
+
</li>
|
|
973
|
+
<li>
|
|
974
|
+
<em>A11y enforced at every scope</em>{' '}
|
|
975
|
+
<span>contrast, focus, semantics, motion, touch targets — never relaxed.</span>
|
|
976
|
+
</li>
|
|
977
|
+
<li>
|
|
978
|
+
Persisted on canvas's <code>.meta.json</code> <code>opt_out_scope</code> field —
|
|
979
|
+
subsequent <code>/design:edit</code> iterations inherit.
|
|
980
|
+
</li>
|
|
981
|
+
<li>
|
|
982
|
+
Inferred from brief ("modern", "vibrant", "off-system") with one-shot
|
|
983
|
+
AskUserQuestion before iter-1 critics fire.
|
|
984
|
+
</li>
|
|
985
|
+
</ul>
|
|
986
|
+
</details>
|
|
987
|
+
<details>
|
|
988
|
+
<summary>Auto-critic loop</summary>
|
|
989
|
+
<ul>
|
|
990
|
+
<li>
|
|
991
|
+
<strong>Default</strong>{' '}
|
|
992
|
+
<span>4 iter · aspiration ≥ 4.0 · stable-but-bland exit</span>
|
|
993
|
+
</li>
|
|
994
|
+
<li>
|
|
995
|
+
<strong>--perfect</strong>{' '}
|
|
996
|
+
<span>8 iter · aspiration ≥ 4.5 · broader divergence tolerance</span>
|
|
997
|
+
</li>
|
|
998
|
+
<li>
|
|
999
|
+
<strong>--perfect --all</strong>{' '}
|
|
1000
|
+
<span>every critic incl. aspiration · portfolio-grade</span>
|
|
1001
|
+
</li>
|
|
1002
|
+
<li>
|
|
1003
|
+
Exit: <code>solid</code> · <code>stable-but-bland</code> · <code>max-reached</code>{' '}
|
|
1004
|
+
· <code>divergent</code>
|
|
1005
|
+
</li>
|
|
1006
|
+
<li>
|
|
1007
|
+
<em>stable-but-bland</em> = correctness clean, aspiration plateau — surface for
|
|
1008
|
+
review with lowest 2 axes named
|
|
1009
|
+
</li>
|
|
1010
|
+
<li>
|
|
1011
|
+
When <code>opt_out_scope ∈ {aesthetic, full}</code>: iter-1 checkpoint
|
|
1012
|
+
fires — pick (a) run loop, (b) skip auto-loop and review iter 1, (c) a11y-only
|
|
1013
|
+
check.
|
|
1014
|
+
</li>
|
|
1015
|
+
</ul>
|
|
1016
|
+
</details>
|
|
1017
|
+
<details>
|
|
1018
|
+
<summary>Pin-to-element flow</summary>
|
|
1019
|
+
<ol>
|
|
1020
|
+
<li>Open canvas tab</li>
|
|
1021
|
+
<li>
|
|
1022
|
+
<kbd>⌘</kbd>+click element
|
|
1023
|
+
</li>
|
|
1024
|
+
<li>Status bar shows ● selector</li>
|
|
1025
|
+
<li>
|
|
1026
|
+
Run{' '}
|
|
1027
|
+
<code>
|
|
1028
|
+
/design:edit "<i>change just this</i>"
|
|
1029
|
+
</code>
|
|
1030
|
+
</li>
|
|
1031
|
+
<li>
|
|
1032
|
+
Reload iframe (<kbd>⌘R</kbd>)
|
|
1033
|
+
</li>
|
|
1034
|
+
</ol>
|
|
1035
|
+
</details>
|
|
774
1036
|
<details>
|
|
775
1037
|
<summary>Comments</summary>
|
|
776
1038
|
<ol>
|
|
777
|
-
<li
|
|
1039
|
+
<li>
|
|
1040
|
+
<kbd>⌘</kbd>+click element, then <kbd>⌘C</kbd> <span>or ⌘⇧+click</span>
|
|
1041
|
+
</li>
|
|
778
1042
|
<li>Numbered pin appears on canvas</li>
|
|
779
|
-
<li
|
|
780
|
-
|
|
781
|
-
|
|
1043
|
+
<li>
|
|
1044
|
+
<kbd>⌘⇧M</kbd> <span>opens panel — All / Open / Resolved</span>
|
|
1045
|
+
</li>
|
|
1046
|
+
<li>
|
|
1047
|
+
Click row in panel <span>jumps to that file + pin</span>
|
|
1048
|
+
</li>
|
|
1049
|
+
<li>
|
|
1050
|
+
Claude reads <code>_comments/<slug>.json</code> on next <code>/design</code>
|
|
1051
|
+
</li>
|
|
782
1052
|
</ol>
|
|
783
1053
|
</details>
|
|
784
1054
|
</div>
|
|
@@ -799,7 +1069,9 @@ const MENU_NAMES = ['File', 'Edit', 'View', 'Selection', 'Tools', 'Help'];
|
|
|
799
1069
|
|
|
800
1070
|
function ViewDropdown({ panels, onToggle, onClose }) {
|
|
801
1071
|
useEffect(() => {
|
|
802
|
-
function onKey(e) {
|
|
1072
|
+
function onKey(e) {
|
|
1073
|
+
if (e.key === 'Escape') onClose();
|
|
1074
|
+
}
|
|
803
1075
|
function onDocClick(e) {
|
|
804
1076
|
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
805
1077
|
}
|
|
@@ -814,32 +1086,39 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
814
1086
|
return (
|
|
815
1087
|
<div className="mb-dropdown" role="menu" aria-label="View" style={{ left: '146px' }}>
|
|
816
1088
|
<div className="mb-dd-hd">Panels</div>
|
|
817
|
-
{panels.map(p => (
|
|
1089
|
+
{panels.map((p) => (
|
|
818
1090
|
<button
|
|
819
1091
|
key={p.id}
|
|
820
1092
|
type="button"
|
|
821
1093
|
role="menuitem"
|
|
822
1094
|
className={'mb-dd-item' + (p.checked ? ' active' : '')}
|
|
823
1095
|
aria-disabled={p.disabled ? 'true' : undefined}
|
|
824
|
-
onClick={() => {
|
|
1096
|
+
onClick={() => {
|
|
1097
|
+
if (!p.disabled) {
|
|
1098
|
+
onToggle(p.id);
|
|
1099
|
+
onClose();
|
|
1100
|
+
}
|
|
1101
|
+
}}
|
|
825
1102
|
>
|
|
826
1103
|
<span className="lbl">
|
|
827
1104
|
<span className="check">{p.checked ? '✓' : ''}</span>
|
|
828
1105
|
<span>{p.label}</span>
|
|
829
1106
|
</span>
|
|
830
|
-
{p.phase
|
|
831
|
-
|
|
832
|
-
|
|
1107
|
+
{p.phase ? (
|
|
1108
|
+
<span className="phase-tag">{p.phase}</span>
|
|
1109
|
+
) : (
|
|
1110
|
+
<span className="shortcut">{p.shortcut || ''}</span>
|
|
1111
|
+
)}
|
|
833
1112
|
</button>
|
|
834
1113
|
))}
|
|
835
1114
|
<div className="mb-dd-sep" />
|
|
836
1115
|
<div className="mb-dd-hd">Zoom</div>
|
|
837
1116
|
{[
|
|
838
|
-
{ label: 'Zoom In',
|
|
839
|
-
{ label: 'Zoom Out',
|
|
1117
|
+
{ label: 'Zoom In', shortcut: '⌘ +' },
|
|
1118
|
+
{ label: 'Zoom Out', shortcut: '⌘ −' },
|
|
840
1119
|
{ label: 'Fit to Screen', shortcut: '⌘ 0' },
|
|
841
1120
|
{ label: 'Actual Size · 100 %', shortcut: '⌥ ⌘ 0' },
|
|
842
|
-
].map(z => (
|
|
1121
|
+
].map((z) => (
|
|
843
1122
|
<button
|
|
844
1123
|
key={z.label}
|
|
845
1124
|
type="button"
|
|
@@ -847,7 +1126,10 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
847
1126
|
className="mb-dd-item"
|
|
848
1127
|
aria-disabled="true"
|
|
849
1128
|
>
|
|
850
|
-
<span className="lbl"
|
|
1129
|
+
<span className="lbl">
|
|
1130
|
+
<span className="check" />
|
|
1131
|
+
<span>{z.label}</span>
|
|
1132
|
+
</span>
|
|
851
1133
|
<span className="phase-tag">Phase 4</span>
|
|
852
1134
|
</button>
|
|
853
1135
|
))}
|
|
@@ -860,7 +1142,9 @@ function ViewDropdown({ panels, onToggle, onClose }) {
|
|
|
860
1142
|
|
|
861
1143
|
function SelectionDropdown({ onAction, onClose }) {
|
|
862
1144
|
useEffect(() => {
|
|
863
|
-
function onKey(e) {
|
|
1145
|
+
function onKey(e) {
|
|
1146
|
+
if (e.key === 'Escape') onClose();
|
|
1147
|
+
}
|
|
864
1148
|
function onDocClick(e) {
|
|
865
1149
|
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
866
1150
|
}
|
|
@@ -872,20 +1156,26 @@ function SelectionDropdown({ onAction, onClose }) {
|
|
|
872
1156
|
};
|
|
873
1157
|
}, [onClose]);
|
|
874
1158
|
const items = [
|
|
875
|
-
{ id: 'deselect-all',
|
|
1159
|
+
{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
|
|
876
1160
|
{ id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⌘ ⇧ A' },
|
|
877
1161
|
];
|
|
878
1162
|
return (
|
|
879
1163
|
<div className="mb-dropdown" role="menu" aria-label="Selection" style={{ left: '195px' }}>
|
|
880
|
-
{items.map(it => (
|
|
1164
|
+
{items.map((it) => (
|
|
881
1165
|
<button
|
|
882
1166
|
key={it.id}
|
|
883
1167
|
type="button"
|
|
884
1168
|
role="menuitem"
|
|
885
1169
|
className="mb-dd-item"
|
|
886
|
-
onClick={() => {
|
|
1170
|
+
onClick={() => {
|
|
1171
|
+
onAction(it.id);
|
|
1172
|
+
onClose();
|
|
1173
|
+
}}
|
|
887
1174
|
>
|
|
888
|
-
<span className="lbl"
|
|
1175
|
+
<span className="lbl">
|
|
1176
|
+
<span className="check" />
|
|
1177
|
+
<span>{it.label}</span>
|
|
1178
|
+
</span>
|
|
889
1179
|
<span className="shortcut">{it.shortcut}</span>
|
|
890
1180
|
</button>
|
|
891
1181
|
))}
|
|
@@ -895,7 +1185,9 @@ function SelectionDropdown({ onAction, onClose }) {
|
|
|
895
1185
|
|
|
896
1186
|
function ToolsDropdown({ onAction, onClose }) {
|
|
897
1187
|
useEffect(() => {
|
|
898
|
-
function onKey(e) {
|
|
1188
|
+
function onKey(e) {
|
|
1189
|
+
if (e.key === 'Escape') onClose();
|
|
1190
|
+
}
|
|
899
1191
|
function onDocClick(e) {
|
|
900
1192
|
if (!e.target.closest('.mb-dropdown, .mb-menu')) onClose();
|
|
901
1193
|
}
|
|
@@ -910,28 +1202,34 @@ function ToolsDropdown({ onAction, onClose }) {
|
|
|
910
1202
|
// kept in sync by hand because the menubar lives in the dev-server shell
|
|
911
1203
|
// (no shared bundle with the canvas iframes).
|
|
912
1204
|
const tools = [
|
|
913
|
-
{ id: 'move',
|
|
914
|
-
{ id: 'hand',
|
|
1205
|
+
{ id: 'move', label: 'Move', shortcut: 'V' },
|
|
1206
|
+
{ id: 'hand', label: 'Hand', shortcut: 'H' },
|
|
915
1207
|
{ id: 'comment', label: 'Comment', shortcut: 'C' },
|
|
916
|
-
{ id: 'pen',
|
|
917
|
-
{ id: 'rect',
|
|
1208
|
+
{ id: 'pen', label: 'Pen', shortcut: 'B' },
|
|
1209
|
+
{ id: 'rect', label: 'Rect', shortcut: 'R' },
|
|
918
1210
|
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O' },
|
|
919
|
-
{ id: 'sticky',
|
|
920
|
-
{ id: 'arrow',
|
|
921
|
-
{ id: 'text',
|
|
922
|
-
{ id: 'eraser',
|
|
1211
|
+
{ id: 'sticky', label: 'Sticky', shortcut: 'N' },
|
|
1212
|
+
{ id: 'arrow', label: 'Arrow', shortcut: 'A' },
|
|
1213
|
+
{ id: 'text', label: 'Text', shortcut: 'T' },
|
|
1214
|
+
{ id: 'eraser', label: 'Eraser', shortcut: 'E' },
|
|
923
1215
|
];
|
|
924
1216
|
return (
|
|
925
1217
|
<div className="mb-dropdown" role="menu" aria-label="Tools" style={{ left: '253px' }}>
|
|
926
|
-
{tools.map(t => (
|
|
1218
|
+
{tools.map((t) => (
|
|
927
1219
|
<button
|
|
928
1220
|
key={t.id}
|
|
929
1221
|
type="button"
|
|
930
1222
|
role="menuitem"
|
|
931
1223
|
className="mb-dd-item"
|
|
932
|
-
onClick={() => {
|
|
1224
|
+
onClick={() => {
|
|
1225
|
+
onAction(t.id);
|
|
1226
|
+
onClose();
|
|
1227
|
+
}}
|
|
933
1228
|
>
|
|
934
|
-
<span className="lbl"
|
|
1229
|
+
<span className="lbl">
|
|
1230
|
+
<span className="check" />
|
|
1231
|
+
<span>{t.label}</span>
|
|
1232
|
+
</span>
|
|
935
1233
|
<span className="shortcut">{t.shortcut}</span>
|
|
936
1234
|
</button>
|
|
937
1235
|
))}
|
|
@@ -939,21 +1237,62 @@ function ToolsDropdown({ onAction, onClose }) {
|
|
|
939
1237
|
);
|
|
940
1238
|
}
|
|
941
1239
|
|
|
942
|
-
function Menubar({
|
|
1240
|
+
function Menubar({
|
|
1241
|
+
activePath,
|
|
1242
|
+
project,
|
|
1243
|
+
tabsCount,
|
|
1244
|
+
openMenu,
|
|
1245
|
+
setOpenMenu,
|
|
1246
|
+
commentsPanelOpen,
|
|
1247
|
+
onToggleComments,
|
|
1248
|
+
onOpenSystem,
|
|
1249
|
+
sidebarOpen,
|
|
1250
|
+
onToggleSidebar,
|
|
1251
|
+
showHidden,
|
|
1252
|
+
onToggleShowHidden,
|
|
1253
|
+
onOpenHelp,
|
|
1254
|
+
annotationsVisible,
|
|
1255
|
+
onToggleAnnotations,
|
|
1256
|
+
postToActiveCanvas,
|
|
1257
|
+
}) {
|
|
943
1258
|
const isSystem = activePath === SYSTEM_TAB;
|
|
944
|
-
const stamp = isSystem ? 'SYSTEM' :
|
|
945
|
-
const fileLabel = isSystem
|
|
946
|
-
|
|
947
|
-
|
|
1259
|
+
const stamp = isSystem ? 'SYSTEM' : activePath ? 'CANVAS' : 'IDLE';
|
|
1260
|
+
const fileLabel = isSystem ? (
|
|
1261
|
+
<b>design system</b>
|
|
1262
|
+
) : activePath ? (
|
|
1263
|
+
<>
|
|
1264
|
+
{activePath.split('/').slice(0, -1).join('/')}/<b>{displayName(basename(activePath))}</b>
|
|
1265
|
+
</>
|
|
1266
|
+
) : (
|
|
1267
|
+
<span style={{ color: 'var(--u-fg-3)' }}>no canvas open</span>
|
|
1268
|
+
);
|
|
948
1269
|
|
|
949
1270
|
const panels = [
|
|
950
|
-
{ id: 'tree',
|
|
951
|
-
{
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1271
|
+
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
1272
|
+
{
|
|
1273
|
+
id: 'comments',
|
|
1274
|
+
label: 'Comments Sidebar',
|
|
1275
|
+
shortcut: '⌘ ⇧ M',
|
|
1276
|
+
checked: commentsPanelOpen,
|
|
1277
|
+
disabled: false,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: 'hidden',
|
|
1281
|
+
label: 'Show hidden files',
|
|
1282
|
+
shortcut: 'H',
|
|
1283
|
+
checked: showHidden,
|
|
1284
|
+
disabled: false,
|
|
1285
|
+
},
|
|
1286
|
+
{ id: 'layers', label: 'Layers Panel', phase: 'Phase 12', disabled: true },
|
|
1287
|
+
{ id: 'inspector', label: 'Inspector', phase: 'Phase 12', disabled: true },
|
|
1288
|
+
{
|
|
1289
|
+
id: 'annotate',
|
|
1290
|
+
label: 'Annotations',
|
|
1291
|
+
shortcut: '⇧ P',
|
|
1292
|
+
checked: annotationsVisible,
|
|
1293
|
+
disabled: false,
|
|
1294
|
+
},
|
|
1295
|
+
{ id: 'present', label: 'Presentation Mode', phase: 'Phase 6', disabled: true },
|
|
957
1296
|
];
|
|
958
1297
|
|
|
959
1298
|
function onMenuClick(key) {
|
|
@@ -972,7 +1311,7 @@ function Menubar({ activePath, project, tabsCount, openMenu, setOpenMenu, commen
|
|
|
972
1311
|
<span>maude</span>
|
|
973
1312
|
</span>
|
|
974
1313
|
<nav className="mb-menus" aria-label="Application menus">
|
|
975
|
-
{MENU_NAMES.map(name => {
|
|
1314
|
+
{MENU_NAMES.map((name) => {
|
|
976
1315
|
const key = name.toLowerCase();
|
|
977
1316
|
const hasDropdown = key === 'view' || key === 'selection' || key === 'tools';
|
|
978
1317
|
const interactive = hasDropdown || key === 'help';
|
|
@@ -997,7 +1336,7 @@ function Menubar({ activePath, project, tabsCount, openMenu, setOpenMenu, commen
|
|
|
997
1336
|
{openMenu === 'view' && (
|
|
998
1337
|
<ViewDropdown
|
|
999
1338
|
panels={panels}
|
|
1000
|
-
onToggle={id => {
|
|
1339
|
+
onToggle={(id) => {
|
|
1001
1340
|
if (id === 'tree') onToggleSidebar();
|
|
1002
1341
|
else if (id === 'comments') onToggleComments();
|
|
1003
1342
|
else if (id === 'hidden') onToggleShowHidden();
|
|
@@ -1010,7 +1349,8 @@ function Menubar({ activePath, project, tabsCount, openMenu, setOpenMenu, commen
|
|
|
1010
1349
|
<SelectionDropdown
|
|
1011
1350
|
onAction={(id) => {
|
|
1012
1351
|
if (id === 'deselect-all') postToActiveCanvas({ dgn: 'selection-clear' });
|
|
1013
|
-
else if (id === 'select-all-annotations')
|
|
1352
|
+
else if (id === 'select-all-annotations')
|
|
1353
|
+
postToActiveCanvas({ dgn: 'annotation-select-all' });
|
|
1014
1354
|
}}
|
|
1015
1355
|
onClose={() => setOpenMenu(null)}
|
|
1016
1356
|
/>
|
|
@@ -1024,13 +1364,21 @@ function Menubar({ activePath, project, tabsCount, openMenu, setOpenMenu, commen
|
|
|
1024
1364
|
<div className="mb-spacer" />
|
|
1025
1365
|
<div className="mb-status">
|
|
1026
1366
|
<span className="cv-stamp">{stamp}</span>
|
|
1027
|
-
<span className="file" title={activePath || ''}>
|
|
1367
|
+
<span className="file" title={activePath || ''}>
|
|
1368
|
+
{fileLabel}
|
|
1369
|
+
</span>
|
|
1028
1370
|
<span className="sep" />
|
|
1029
|
-
<span
|
|
1371
|
+
<span>
|
|
1372
|
+
<span className="accent-dot">●</span> <b>{tabsCount}</b> ARTBOARDS
|
|
1373
|
+
</span>
|
|
1030
1374
|
<span className="sep" />
|
|
1031
|
-
<span title="Pan/zoom in Phase 4">
|
|
1375
|
+
<span title="Pan/zoom in Phase 4">
|
|
1376
|
+
ZOOM <b>100%</b>
|
|
1377
|
+
</span>
|
|
1032
1378
|
<span className="sep" />
|
|
1033
|
-
<span className="ok"
|
|
1379
|
+
<span className="ok">
|
|
1380
|
+
<b>{project || 'MDCC'}</b>
|
|
1381
|
+
</span>
|
|
1034
1382
|
</div>
|
|
1035
1383
|
</header>
|
|
1036
1384
|
);
|
|
@@ -1039,7 +1387,8 @@ function Menubar({ activePath, project, tabsCount, openMenu, setOpenMenu, commen
|
|
|
1039
1387
|
function ThemeToggle({ theme, onToggle }) {
|
|
1040
1388
|
// Show the icon of the theme you'll switch TO — clearer affordance than current state.
|
|
1041
1389
|
// Sun + Moon paths are condensed Lucide-style (single-path so the existing <Icon> works).
|
|
1042
|
-
const sun =
|
|
1390
|
+
const sun =
|
|
1391
|
+
'M12 7a5 5 0 100 10 5 5 0 000-10z M12 3v1 M12 20v1 M21 12h-1 M4 12H3 M16.95 7.05l-.71.71 M7.05 16.95l-.71.71 M16.95 16.95l-.71-.71 M7.05 7.05l-.71-.71';
|
|
1043
1392
|
const moon = 'M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z';
|
|
1044
1393
|
const next = theme === 'dark' ? 'light' : 'dark';
|
|
1045
1394
|
return (
|
|
@@ -1071,38 +1420,64 @@ function Wordmark({ project, port, version }) {
|
|
|
1071
1420
|
);
|
|
1072
1421
|
}
|
|
1073
1422
|
|
|
1074
|
-
function Viewport({
|
|
1423
|
+
function Viewport({
|
|
1424
|
+
tabs,
|
|
1425
|
+
activePath,
|
|
1426
|
+
registerIframe,
|
|
1427
|
+
systemData,
|
|
1428
|
+
onOpenFromSystem,
|
|
1429
|
+
onSelectDs,
|
|
1430
|
+
project,
|
|
1431
|
+
cfg,
|
|
1432
|
+
}) {
|
|
1075
1433
|
return (
|
|
1076
1434
|
<div className="viewport">
|
|
1077
1435
|
{tabs.length === 0 && (
|
|
1078
1436
|
<>
|
|
1079
|
-
<Wordmark
|
|
1437
|
+
<Wordmark
|
|
1438
|
+
project={project}
|
|
1439
|
+
port={typeof window !== 'undefined' ? window.location.port : ''}
|
|
1440
|
+
version={MDCC_VERSION}
|
|
1441
|
+
/>
|
|
1080
1442
|
<div className="empty-state">
|
|
1081
1443
|
<div className="big">No mock open</div>
|
|
1082
1444
|
<div className="small">
|
|
1083
|
-
← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
<br
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1445
|
+
← Click a <code>.tsx</code> (or legacy <code>.html</code>) file in the tree, or open
|
|
1446
|
+
the <strong>Design system</strong> view above it.
|
|
1447
|
+
<br />
|
|
1448
|
+
<br />
|
|
1449
|
+
Tabs work like in an editor — close with the × on each tab. <kbd>⌘R</kbd> reloads the
|
|
1450
|
+
active iframe.
|
|
1451
|
+
<br />
|
|
1452
|
+
<br />
|
|
1453
|
+
<strong>Element selection:</strong> hold <kbd>⌘</kbd> inside the canvas and hover for
|
|
1454
|
+
a preview, click to select. <kbd>⌘⇧</kbd>+click adds to a multi-selection.{' '}
|
|
1455
|
+
<kbd>V</kbd>/<kbd>H</kbd>/<kbd>C</kbd> swap tool; right-click opens the context menu.
|
|
1456
|
+
<br />
|
|
1457
|
+
<br />
|
|
1458
|
+
Active file, selection, and comments are tracked in <code>_active.json</code> +{' '}
|
|
1459
|
+
<code>_comments/</code> — Claude reads them when you run <code>/design</code>.
|
|
1090
1460
|
</div>
|
|
1091
1461
|
</div>
|
|
1092
1462
|
</>
|
|
1093
1463
|
)}
|
|
1094
|
-
{tabs.map(t => {
|
|
1464
|
+
{tabs.map((t) => {
|
|
1095
1465
|
if (t.path === SYSTEM_TAB) {
|
|
1096
1466
|
return (
|
|
1097
1467
|
<div key={t.path} className={'system-view' + (t.path === activePath ? ' active' : '')}>
|
|
1098
|
-
<SystemView
|
|
1468
|
+
<SystemView
|
|
1469
|
+
data={systemData}
|
|
1470
|
+
onOpen={onOpenFromSystem}
|
|
1471
|
+
cfg={cfg}
|
|
1472
|
+
onSelectDs={onSelectDs}
|
|
1473
|
+
/>
|
|
1099
1474
|
</div>
|
|
1100
1475
|
);
|
|
1101
1476
|
}
|
|
1102
1477
|
return (
|
|
1103
1478
|
<iframe
|
|
1104
1479
|
key={t.path}
|
|
1105
|
-
ref={el => registerIframe(t.path, el)}
|
|
1480
|
+
ref={(el) => registerIframe(t.path, el)}
|
|
1106
1481
|
src={canvasUrl(t.path, cfg)}
|
|
1107
1482
|
className={t.path === activePath ? 'active' : ''}
|
|
1108
1483
|
data-path={t.path}
|
|
@@ -1133,7 +1508,15 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
|
|
|
1133
1508
|
// Order kinds match the typical reading flow of a tokens file. Unknown kinds
|
|
1134
1509
|
// fall through to `other` so a custom token group still renders, just last.
|
|
1135
1510
|
const TOKEN_GROUP_ORDER = [
|
|
1136
|
-
'color',
|
|
1511
|
+
'color',
|
|
1512
|
+
'space',
|
|
1513
|
+
'radius',
|
|
1514
|
+
'shadow',
|
|
1515
|
+
'leading',
|
|
1516
|
+
'weight',
|
|
1517
|
+
'motion',
|
|
1518
|
+
'font',
|
|
1519
|
+
'other',
|
|
1137
1520
|
];
|
|
1138
1521
|
const TOKEN_GROUP_LABELS = {
|
|
1139
1522
|
color: 'colors',
|
|
@@ -1147,17 +1530,22 @@ const TOKEN_GROUP_LABELS = {
|
|
|
1147
1530
|
other: 'other',
|
|
1148
1531
|
};
|
|
1149
1532
|
|
|
1150
|
-
function isSwatchKind(kind) {
|
|
1533
|
+
function isSwatchKind(kind) {
|
|
1534
|
+
return kind === 'color';
|
|
1535
|
+
}
|
|
1151
1536
|
|
|
1152
1537
|
function TokenLadder({ tokens, tokenGroups, tokensPath }) {
|
|
1153
1538
|
if (!tokens || tokens.length === 0) {
|
|
1154
1539
|
return (
|
|
1155
1540
|
<section className="sv-section sv-section-tokens">
|
|
1156
|
-
<h2>
|
|
1541
|
+
<h2>
|
|
1542
|
+
tokens<span className="sv-h-num">0</span>
|
|
1543
|
+
</h2>
|
|
1157
1544
|
<div className="sv-empty">
|
|
1158
1545
|
<p>
|
|
1159
|
-
No tokens parsed from
|
|
1160
|
-
|
|
1546
|
+
No tokens parsed from{' '}
|
|
1547
|
+
{tokensPath ? <code>{tokensPath}</code> : 'the configured tokens file'}. Does the file
|
|
1548
|
+
exist and contain CSS custom properties (<code>--name: value;</code>)?
|
|
1161
1549
|
</p>
|
|
1162
1550
|
</div>
|
|
1163
1551
|
</section>
|
|
@@ -1165,9 +1553,9 @@ function TokenLadder({ tokens, tokenGroups, tokensPath }) {
|
|
|
1165
1553
|
}
|
|
1166
1554
|
|
|
1167
1555
|
const groups = tokenGroups || {};
|
|
1168
|
-
const kinds = Array.from(
|
|
1169
|
-
|
|
1170
|
-
)
|
|
1556
|
+
const kinds = Array.from(new Set([...TOKEN_GROUP_ORDER, ...Object.keys(groups)])).filter(
|
|
1557
|
+
(k) => groups[k]?.length
|
|
1558
|
+
);
|
|
1171
1559
|
|
|
1172
1560
|
return (
|
|
1173
1561
|
<>
|
|
@@ -1206,9 +1594,11 @@ function findLeadingFor(typeToken, leadingTokens) {
|
|
|
1206
1594
|
const m = typeToken.name.match(/^--(?:type|fs|text)-(.+)$/);
|
|
1207
1595
|
if (!m) return null;
|
|
1208
1596
|
const suffix = m[1];
|
|
1209
|
-
return (
|
|
1210
|
-
|
|
1211
|
-
|
|
1597
|
+
return (
|
|
1598
|
+
(leadingTokens || []).find(
|
|
1599
|
+
(t) => /^--(?:lh|leading|line-height)-/.test(t.name) && t.name.endsWith('-' + suffix)
|
|
1600
|
+
)?.value ?? null
|
|
1601
|
+
);
|
|
1212
1602
|
}
|
|
1213
1603
|
|
|
1214
1604
|
// Best-effort sample font: prefer body / sans / display tokens, fall back to
|
|
@@ -1257,7 +1647,11 @@ function TypeLadder({ tokenGroups }) {
|
|
|
1257
1647
|
|
|
1258
1648
|
function SystemView({ data, onOpen, cfg, onSelectDs }) {
|
|
1259
1649
|
if (!data) {
|
|
1260
|
-
return
|
|
1650
|
+
return (
|
|
1651
|
+
<div className="sv-empty">
|
|
1652
|
+
<p>Loading design system…</p>
|
|
1653
|
+
</div>
|
|
1654
|
+
);
|
|
1261
1655
|
}
|
|
1262
1656
|
const {
|
|
1263
1657
|
previewGallery,
|
|
@@ -1269,7 +1663,8 @@ function SystemView({ data, onOpen, cfg, onSelectDs }) {
|
|
|
1269
1663
|
ds,
|
|
1270
1664
|
availableDesignSystems,
|
|
1271
1665
|
} = data;
|
|
1272
|
-
const empty =
|
|
1666
|
+
const empty =
|
|
1667
|
+
(!previewGallery || !previewGallery.length) && (!uiKitsGallery || !uiKitsGallery.length);
|
|
1273
1668
|
const hasPicker = Array.isArray(availableDesignSystems) && availableDesignSystems.length > 1;
|
|
1274
1669
|
const selectedName = ds?.name ?? availableDesignSystems?.[0]?.name ?? '';
|
|
1275
1670
|
|
|
@@ -1281,34 +1676,42 @@ function SystemView({ data, onOpen, cfg, onSelectDs }) {
|
|
|
1281
1676
|
{hasPicker ? (
|
|
1282
1677
|
<label className="sv-ds-picker">
|
|
1283
1678
|
<span className="sv-ds-picker-label">DS</span>
|
|
1284
|
-
<select
|
|
1285
|
-
value={selectedName}
|
|
1286
|
-
onChange={(e) => onSelectDs && onSelectDs(e.target.value)}
|
|
1287
|
-
>
|
|
1679
|
+
<select value={selectedName} onChange={(e) => onSelectDs && onSelectDs(e.target.value)}>
|
|
1288
1680
|
{availableDesignSystems.map((d) => (
|
|
1289
|
-
<option key={d.name} value={d.name}>
|
|
1681
|
+
<option key={d.name} value={d.name}>
|
|
1682
|
+
{d.name}
|
|
1683
|
+
</option>
|
|
1290
1684
|
))}
|
|
1291
1685
|
</select>
|
|
1292
1686
|
</label>
|
|
1293
1687
|
) : null}
|
|
1294
|
-
<span className="sv-loc"
|
|
1688
|
+
<span className="sv-loc">
|
|
1689
|
+
<code>{systemDir}</code>
|
|
1690
|
+
</span>
|
|
1295
1691
|
</header>
|
|
1296
1692
|
|
|
1297
|
-
{ds?.description ?
|
|
1298
|
-
<p className="sv-ds-description">{ds.description}</p>
|
|
1299
|
-
) : null}
|
|
1693
|
+
{ds?.description ? <p className="sv-ds-description">{ds.description}</p> : null}
|
|
1300
1694
|
|
|
1301
1695
|
<TokenLadder tokens={tokens} tokenGroups={tokenGroups} tokensPath={tokensPath} />
|
|
1302
1696
|
<TypeLadder tokenGroups={tokenGroups} />
|
|
1303
1697
|
|
|
1304
1698
|
{empty ? (
|
|
1305
1699
|
<div className="sv-empty">
|
|
1306
|
-
<p>
|
|
1700
|
+
<p>
|
|
1701
|
+
No <code>preview/</code> or <code>ui_kits/</code> folders found under{' '}
|
|
1702
|
+
<code>{systemDir}</code>.
|
|
1703
|
+
</p>
|
|
1307
1704
|
</div>
|
|
1308
1705
|
) : (
|
|
1309
1706
|
<>
|
|
1310
|
-
<Gallery
|
|
1311
|
-
|
|
1707
|
+
<Gallery
|
|
1708
|
+
title="preview"
|
|
1709
|
+
items={previewGallery}
|
|
1710
|
+
onOpen={onOpen}
|
|
1711
|
+
kind="preview"
|
|
1712
|
+
cfg={cfg}
|
|
1713
|
+
/>
|
|
1714
|
+
<Gallery title="ui kits" items={uiKitsGallery} onOpen={onOpen} kind="ui_kits" cfg={cfg} />
|
|
1312
1715
|
</>
|
|
1313
1716
|
)}
|
|
1314
1717
|
</div>
|
|
@@ -1319,12 +1722,19 @@ function Gallery({ title, items, onOpen, kind, cfg }) {
|
|
|
1319
1722
|
if (!items || items.length === 0) return null;
|
|
1320
1723
|
return (
|
|
1321
1724
|
<section className="sv-section">
|
|
1322
|
-
<h2>
|
|
1725
|
+
<h2>
|
|
1726
|
+
{title} <span className="sv-count">{items.length}</span>
|
|
1727
|
+
</h2>
|
|
1323
1728
|
<div className={'sv-previews sv-previews-' + kind}>
|
|
1324
|
-
{items.map(p => (
|
|
1729
|
+
{items.map((p) => (
|
|
1325
1730
|
<article key={p.path} className="sv-preview-card" onClick={() => onOpen(p.path)}>
|
|
1326
1731
|
<div className="sv-preview-frame">
|
|
1327
|
-
<iframe
|
|
1732
|
+
<iframe
|
|
1733
|
+
src={canvasUrl(p.path, cfg, { thumbnail: true })}
|
|
1734
|
+
title={p.label}
|
|
1735
|
+
scrolling="no"
|
|
1736
|
+
{...(cfg?.canvasOrigin ? { sandbox: 'allow-scripts allow-same-origin' } : {})}
|
|
1737
|
+
/>
|
|
1328
1738
|
</div>
|
|
1329
1739
|
<div className="sv-preview-foot">
|
|
1330
1740
|
<strong>{p.label}</strong>
|
|
@@ -1345,7 +1755,7 @@ function CommentBar({ activePath, comments }) {
|
|
|
1345
1755
|
// bubbles + thread popover. BottomBar shrinks to a live open-count summary
|
|
1346
1756
|
// so the shell still surfaces total review activity at a glance.
|
|
1347
1757
|
if (!activePath) return null;
|
|
1348
|
-
const openComments = (comments || []).filter(c => c.status !== 'resolved');
|
|
1758
|
+
const openComments = (comments || []).filter((c) => c.status !== 'resolved');
|
|
1349
1759
|
if (openComments.length === 0) return null;
|
|
1350
1760
|
return (
|
|
1351
1761
|
<div className="comment-bar">
|
|
@@ -1366,26 +1776,53 @@ function StatusBarSlot({ label, children, className = '' }) {
|
|
|
1366
1776
|
);
|
|
1367
1777
|
}
|
|
1368
1778
|
|
|
1369
|
-
function StatusBar({
|
|
1779
|
+
function StatusBar({
|
|
1780
|
+
activePath,
|
|
1781
|
+
selected,
|
|
1782
|
+
wsConnected,
|
|
1783
|
+
openCount,
|
|
1784
|
+
theme,
|
|
1785
|
+
onToggleTheme,
|
|
1786
|
+
onClearSelected,
|
|
1787
|
+
syncStatus,
|
|
1788
|
+
}) {
|
|
1370
1789
|
const isSystem = activePath === SYSTEM_TAB;
|
|
1371
|
-
const text =
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1790
|
+
const text =
|
|
1791
|
+
selected && selected.selector
|
|
1792
|
+
? selected.selector + (selected.text ? ` — "${selected.text.slice(0, 60)}"` : '')
|
|
1793
|
+
: '';
|
|
1794
|
+
const title =
|
|
1795
|
+
selected && selected.dom_path
|
|
1796
|
+
? selected.dom_path.join(' > ')
|
|
1797
|
+
: selected
|
|
1798
|
+
? selected.selector
|
|
1799
|
+
: '';
|
|
1375
1800
|
return (
|
|
1376
1801
|
<div className="statusbar" role="contentinfo">
|
|
1377
1802
|
<StatusBarSlot label="Active file" className="sb-active">
|
|
1378
1803
|
<span className="sb-key">active</span>
|
|
1379
1804
|
<span className="sb-file" title={activePath || ''}>
|
|
1380
|
-
{isSystem ? '▦ design system' :
|
|
1805
|
+
{isSystem ? '▦ design system' : activePath || '—'}
|
|
1381
1806
|
</span>
|
|
1382
1807
|
</StatusBarSlot>
|
|
1383
1808
|
|
|
1384
1809
|
{selected && selected.selector && !isSystem && (
|
|
1385
1810
|
<StatusBarSlot label="Selected element" className="sb-selected">
|
|
1386
|
-
<span className="sb-dot" aria-hidden="true"
|
|
1387
|
-
|
|
1388
|
-
|
|
1811
|
+
<span className="sb-dot" aria-hidden="true">
|
|
1812
|
+
●
|
|
1813
|
+
</span>
|
|
1814
|
+
<span className="sb-sel-text" title={title}>
|
|
1815
|
+
{text}
|
|
1816
|
+
</span>
|
|
1817
|
+
<button
|
|
1818
|
+
type="button"
|
|
1819
|
+
className="sb-clear-sel"
|
|
1820
|
+
onClick={onClearSelected}
|
|
1821
|
+
title="Clear (Esc inside iframe)"
|
|
1822
|
+
aria-label="Clear selection"
|
|
1823
|
+
>
|
|
1824
|
+
×
|
|
1825
|
+
</button>
|
|
1389
1826
|
</StatusBarSlot>
|
|
1390
1827
|
)}
|
|
1391
1828
|
|
|
@@ -1405,7 +1842,10 @@ function StatusBar({ activePath, selected, wsConnected, openCount, theme, onTogg
|
|
|
1405
1842
|
{syncStatus?.notSyncable && (
|
|
1406
1843
|
<StatusBarSlot label="Hub sync" className="sb-sync">
|
|
1407
1844
|
<span className="sb-sync-dot" aria-hidden="true" />
|
|
1408
|
-
<span
|
|
1845
|
+
<span
|
|
1846
|
+
className="sb-key"
|
|
1847
|
+
title={syncStatus.reason || 'Linked to a hub, but no canvases are syncable.'}
|
|
1848
|
+
>
|
|
1409
1849
|
0 syncable{syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}
|
|
1410
1850
|
</span>
|
|
1411
1851
|
</StatusBarSlot>
|
|
@@ -1422,26 +1862,36 @@ function StatusBar({ activePath, selected, wsConnected, openCount, theme, onTogg
|
|
|
1422
1862
|
|
|
1423
1863
|
// ---------- Right sidebar — Comments panel ----------
|
|
1424
1864
|
|
|
1425
|
-
function CommentsPanel({
|
|
1865
|
+
function CommentsPanel({
|
|
1866
|
+
commentsByFile,
|
|
1867
|
+
filter,
|
|
1868
|
+
setFilter,
|
|
1869
|
+
activePath,
|
|
1870
|
+
focusedId,
|
|
1871
|
+
onJump,
|
|
1872
|
+
onResolve,
|
|
1873
|
+
onReopen,
|
|
1874
|
+
onDelete,
|
|
1875
|
+
}) {
|
|
1426
1876
|
const counts = totalCounts(commentsByFile);
|
|
1427
1877
|
// Build groups: [{ file, comments: filtered }]
|
|
1428
1878
|
const files = Object.keys(commentsByFile || {}).sort();
|
|
1429
1879
|
const groups = [];
|
|
1430
1880
|
for (const f of files) {
|
|
1431
1881
|
const all = commentsByFile[f] || [];
|
|
1432
|
-
const filtered = all.filter(c => {
|
|
1882
|
+
const filtered = all.filter((c) => {
|
|
1433
1883
|
if (filter === 'open') return c.status !== 'resolved';
|
|
1434
1884
|
if (filter === 'resolved') return c.status === 'resolved';
|
|
1435
1885
|
return true;
|
|
1436
1886
|
});
|
|
1437
1887
|
if (filtered.length === 0) continue;
|
|
1438
1888
|
// Number is fixed by all-list order so it matches pin numbers (which are based on position in the array of selector-having comments)
|
|
1439
|
-
const numberedAll = all.filter(c => c.selector);
|
|
1889
|
+
const numberedAll = all.filter((c) => c.selector);
|
|
1440
1890
|
groups.push({
|
|
1441
1891
|
file: f,
|
|
1442
|
-
comments: filtered.map(c => ({
|
|
1892
|
+
comments: filtered.map((c) => ({
|
|
1443
1893
|
...c,
|
|
1444
|
-
n: numberedAll.findIndex(x => x.id === c.id) + 1,
|
|
1894
|
+
n: numberedAll.findIndex((x) => x.id === c.id) + 1,
|
|
1445
1895
|
})),
|
|
1446
1896
|
});
|
|
1447
1897
|
}
|
|
@@ -1456,61 +1906,101 @@ function CommentsPanel({ commentsByFile, filter, setFilter, activePath, focusedI
|
|
|
1456
1906
|
<div className="rsidebar-filters" role="tablist">
|
|
1457
1907
|
<button
|
|
1458
1908
|
className={'rsidebar-filter' + (filter === 'all' ? ' active' : '')}
|
|
1459
|
-
role="tab"
|
|
1909
|
+
role="tab"
|
|
1910
|
+
aria-selected={filter === 'all'}
|
|
1460
1911
|
onClick={() => setFilter('all')}
|
|
1461
|
-
>
|
|
1912
|
+
>
|
|
1913
|
+
All <span className="fc">{counts.all}</span>
|
|
1914
|
+
</button>
|
|
1462
1915
|
<button
|
|
1463
1916
|
className={'rsidebar-filter' + (filter === 'open' ? ' active' : '')}
|
|
1464
|
-
role="tab"
|
|
1917
|
+
role="tab"
|
|
1918
|
+
aria-selected={filter === 'open'}
|
|
1465
1919
|
onClick={() => setFilter('open')}
|
|
1466
|
-
>
|
|
1920
|
+
>
|
|
1921
|
+
Open <span className="fc">{counts.open}</span>
|
|
1922
|
+
</button>
|
|
1467
1923
|
<button
|
|
1468
1924
|
className={'rsidebar-filter' + (filter === 'resolved' ? ' active' : '')}
|
|
1469
|
-
role="tab"
|
|
1925
|
+
role="tab"
|
|
1926
|
+
aria-selected={filter === 'resolved'}
|
|
1470
1927
|
onClick={() => setFilter('resolved')}
|
|
1471
|
-
>
|
|
1928
|
+
>
|
|
1929
|
+
Resolved <span className="fc">{counts.resolved}</span>
|
|
1930
|
+
</button>
|
|
1472
1931
|
</div>
|
|
1473
1932
|
</div>
|
|
1474
1933
|
<div className="rsidebar-body">
|
|
1475
1934
|
{groups.length === 0 ? (
|
|
1476
1935
|
<div className="rsidebar-empty">
|
|
1477
1936
|
<p>No comments {filter !== 'all' ? `with status “${filter}”` : 'yet'}.</p>
|
|
1478
|
-
<p style={{ marginTop: 12 }}>
|
|
1937
|
+
<p style={{ marginTop: 12 }}>
|
|
1938
|
+
Open a canvas, hold <kbd>⌘</kbd> and click an element, then press <kbd>C</kbd> — or
|
|
1939
|
+
hold <kbd>⌘⇧</kbd> and click directly.
|
|
1940
|
+
</p>
|
|
1479
1941
|
</div>
|
|
1480
|
-
) :
|
|
1481
|
-
|
|
1482
|
-
<
|
|
1483
|
-
className="rs-group-h"
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
<
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
<
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1942
|
+
) : (
|
|
1943
|
+
groups.map((g) => (
|
|
1944
|
+
<div key={g.file} className="rs-group">
|
|
1945
|
+
<button className="rs-group-h" onClick={() => onJump(g.file, null)} title={g.file}>
|
|
1946
|
+
<span className="rs-group-name">{displayName(basename(g.file))}</span>
|
|
1947
|
+
<span className="rs-group-count">{g.comments.length}</span>
|
|
1948
|
+
</button>
|
|
1949
|
+
{g.comments.map((c) => (
|
|
1950
|
+
<div
|
|
1951
|
+
key={c.id}
|
|
1952
|
+
className={
|
|
1953
|
+
'rs-comment' +
|
|
1954
|
+
(c.status === 'resolved' ? ' resolved' : '') +
|
|
1955
|
+
(c.id === focusedId ? ' active-pin' : '')
|
|
1956
|
+
}
|
|
1957
|
+
onClick={() => onJump(g.file, c.id)}
|
|
1958
|
+
>
|
|
1959
|
+
<div className="rs-comment-head">
|
|
1960
|
+
<span className="rs-num">{c.n || '·'}</span>
|
|
1961
|
+
<span className="rs-time">{timeAgo(c.created)}</span>
|
|
1962
|
+
</div>
|
|
1963
|
+
<div className="rs-comment-text">{c.text}</div>
|
|
1964
|
+
<div className="rs-comment-foot">
|
|
1965
|
+
<code title={(c.dom_path || []).join(' > ')}>{c.selector || '—'}</code>
|
|
1966
|
+
<div className="rs-comment-actions">
|
|
1967
|
+
{c.status === 'resolved' ? (
|
|
1968
|
+
<button
|
|
1969
|
+
className="rs-act"
|
|
1970
|
+
onClick={(e) => {
|
|
1971
|
+
e.stopPropagation();
|
|
1972
|
+
onReopen(c.id);
|
|
1973
|
+
}}
|
|
1974
|
+
>
|
|
1975
|
+
↺
|
|
1976
|
+
</button>
|
|
1977
|
+
) : (
|
|
1978
|
+
<button
|
|
1979
|
+
className="rs-act"
|
|
1980
|
+
onClick={(e) => {
|
|
1981
|
+
e.stopPropagation();
|
|
1982
|
+
onResolve(c.id);
|
|
1983
|
+
}}
|
|
1984
|
+
>
|
|
1985
|
+
✓
|
|
1986
|
+
</button>
|
|
1987
|
+
)}
|
|
1988
|
+
<button
|
|
1989
|
+
className="rs-act danger"
|
|
1990
|
+
onClick={(e) => {
|
|
1991
|
+
e.stopPropagation();
|
|
1992
|
+
onDelete(c.id);
|
|
1993
|
+
}}
|
|
1994
|
+
>
|
|
1995
|
+
×
|
|
1996
|
+
</button>
|
|
1997
|
+
</div>
|
|
1508
1998
|
</div>
|
|
1509
1999
|
</div>
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
)
|
|
2000
|
+
))}
|
|
2001
|
+
</div>
|
|
2002
|
+
))
|
|
2003
|
+
)}
|
|
1514
2004
|
</div>
|
|
1515
2005
|
</aside>
|
|
1516
2006
|
);
|
|
@@ -1631,7 +2121,9 @@ function App() {
|
|
|
1631
2121
|
});
|
|
1632
2122
|
})
|
|
1633
2123
|
.catch(() => {});
|
|
1634
|
-
return () => {
|
|
2124
|
+
return () => {
|
|
2125
|
+
cancelled = true;
|
|
2126
|
+
};
|
|
1635
2127
|
}, []);
|
|
1636
2128
|
// Backfill the sync banner on mount from /_sync-status. The 'sync:status' WS
|
|
1637
2129
|
// broadcast is one-shot for the zero-syncable case (DDR-060 / 9.1-D), so a
|
|
@@ -1646,14 +2138,16 @@ function App() {
|
|
|
1646
2138
|
setSyncStatus(data);
|
|
1647
2139
|
})
|
|
1648
2140
|
.catch(() => {});
|
|
1649
|
-
return () => {
|
|
2141
|
+
return () => {
|
|
2142
|
+
cancelled = true;
|
|
2143
|
+
};
|
|
1650
2144
|
}, []);
|
|
1651
|
-
const [commentsByFile, setCommentsByFile] = useState({});
|
|
2145
|
+
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
1652
2146
|
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
1653
2147
|
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
1654
2148
|
const [focusedCommentId, setFocusedCommentId] = useState(null);
|
|
1655
2149
|
const [commentsPanelOpen, setCommentsPanelOpen] = useState(false);
|
|
1656
|
-
const [commentsFilter, setCommentsFilter] = useState('open');
|
|
2150
|
+
const [commentsFilter, setCommentsFilter] = useState('open'); // 'all' | 'open' | 'resolved'
|
|
1657
2151
|
const [theme, setTheme] = useState(readInitialTheme);
|
|
1658
2152
|
const [openMenu, setOpenMenu] = useState(null);
|
|
1659
2153
|
const [sidebarOpen, setSidebarOpen] = useState(() => readBoolStore(SIDEBAR_STORE, true));
|
|
@@ -1668,18 +2162,25 @@ function App() {
|
|
|
1668
2162
|
// The canvas-shell listens for these `dgn:*` messages and dispatches into the
|
|
1669
2163
|
// matching local provider (annotations visibility / both selection stores /
|
|
1670
2164
|
// tool mode). Mirrors the existing `force-clear` / `select-clear` channel.
|
|
1671
|
-
const postToActiveCanvas = useCallback(
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2165
|
+
const postToActiveCanvas = useCallback(
|
|
2166
|
+
(payload) => {
|
|
2167
|
+
const el = activePath ? iframesRef.current.get(activePath) : null;
|
|
2168
|
+
if (!el || !el.contentWindow) return;
|
|
2169
|
+
try {
|
|
2170
|
+
el.contentWindow.postMessage(payload, '*');
|
|
2171
|
+
} catch {}
|
|
2172
|
+
},
|
|
2173
|
+
[activePath]
|
|
2174
|
+
);
|
|
1676
2175
|
|
|
1677
2176
|
const toggleAnnotations = useCallback(() => {
|
|
1678
2177
|
setAnnotationsVisible((v) => {
|
|
1679
2178
|
const next = !v;
|
|
1680
2179
|
const el = activePath ? iframesRef.current.get(activePath) : null;
|
|
1681
2180
|
if (el && el.contentWindow) {
|
|
1682
|
-
try {
|
|
2181
|
+
try {
|
|
2182
|
+
el.contentWindow.postMessage({ dgn: 'view-annotations', visible: next }, '*');
|
|
2183
|
+
} catch {}
|
|
1683
2184
|
}
|
|
1684
2185
|
return next;
|
|
1685
2186
|
});
|
|
@@ -1700,23 +2201,31 @@ function App() {
|
|
|
1700
2201
|
// mount run iframesRef is empty (no canvas open yet) — a freshly-loaded
|
|
1701
2202
|
// iframe instead gets the current theme from the `dgn:'loaded'` handler.
|
|
1702
2203
|
for (const el of iframesRef.current.values()) {
|
|
1703
|
-
try {
|
|
2204
|
+
try {
|
|
2205
|
+
el.contentWindow.postMessage({ dgn: 'theme', theme }, '*');
|
|
2206
|
+
} catch {}
|
|
1704
2207
|
}
|
|
1705
2208
|
}, [theme]);
|
|
1706
2209
|
|
|
1707
2210
|
// Persist sidebar / hidden-files / DS-body toggles. Mirror theme pattern.
|
|
1708
2211
|
useEffect(() => {
|
|
1709
|
-
try {
|
|
2212
|
+
try {
|
|
2213
|
+
localStorage.setItem(SIDEBAR_STORE, sidebarOpen ? '1' : '0');
|
|
2214
|
+
} catch {}
|
|
1710
2215
|
}, [sidebarOpen]);
|
|
1711
2216
|
useEffect(() => {
|
|
1712
|
-
try {
|
|
2217
|
+
try {
|
|
2218
|
+
localStorage.setItem(SHOW_HIDDEN_STORE, showHidden ? '1' : '0');
|
|
2219
|
+
} catch {}
|
|
1713
2220
|
}, [showHidden]);
|
|
1714
2221
|
useEffect(() => {
|
|
1715
|
-
try {
|
|
2222
|
+
try {
|
|
2223
|
+
localStorage.setItem(SECTIONS_STORE, JSON.stringify(sectionsExpanded));
|
|
2224
|
+
} catch {}
|
|
1716
2225
|
}, [sectionsExpanded]);
|
|
1717
2226
|
|
|
1718
2227
|
const toggleSection = useCallback((label, defaultOpen) => {
|
|
1719
|
-
setSectionsExpanded(prev => {
|
|
2228
|
+
setSectionsExpanded((prev) => {
|
|
1720
2229
|
const cur = prev[label];
|
|
1721
2230
|
const isOpen = cur === undefined ? defaultOpen : cur;
|
|
1722
2231
|
return { ...prev, [label]: !isOpen };
|
|
@@ -1724,7 +2233,7 @@ function App() {
|
|
|
1724
2233
|
}, []);
|
|
1725
2234
|
|
|
1726
2235
|
const toggleTheme = useCallback(() => {
|
|
1727
|
-
setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
|
2236
|
+
setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
|
|
1728
2237
|
}, []);
|
|
1729
2238
|
|
|
1730
2239
|
// ----- Tree -----
|
|
@@ -1733,7 +2242,7 @@ function App() {
|
|
|
1733
2242
|
const r = await fetch('/_index-data');
|
|
1734
2243
|
const data = await r.json();
|
|
1735
2244
|
setProject(data.project || 'Design');
|
|
1736
|
-
const built = data.groups.map(g => ({
|
|
2245
|
+
const built = data.groups.map((g) => ({
|
|
1737
2246
|
...g,
|
|
1738
2247
|
tree: buildTree(g.paths, g.stripPrefix),
|
|
1739
2248
|
}));
|
|
@@ -1743,7 +2252,9 @@ function App() {
|
|
|
1743
2252
|
}
|
|
1744
2253
|
}, []);
|
|
1745
2254
|
|
|
1746
|
-
useEffect(() => {
|
|
2255
|
+
useEffect(() => {
|
|
2256
|
+
loadTree();
|
|
2257
|
+
}, [loadTree]);
|
|
1747
2258
|
|
|
1748
2259
|
// ----- System data (lazy) -----
|
|
1749
2260
|
// `dsName` scopes to a single design-system entry (DDR-048). The initial
|
|
@@ -1786,7 +2297,9 @@ function App() {
|
|
|
1786
2297
|
}
|
|
1787
2298
|
}, []);
|
|
1788
2299
|
|
|
1789
|
-
useEffect(() => {
|
|
2300
|
+
useEffect(() => {
|
|
2301
|
+
loadAllComments();
|
|
2302
|
+
}, [loadAllComments]);
|
|
1790
2303
|
|
|
1791
2304
|
// ----- WebSocket -----
|
|
1792
2305
|
useEffect(() => {
|
|
@@ -1800,7 +2313,7 @@ function App() {
|
|
|
1800
2313
|
setTimeout(connect, 1000);
|
|
1801
2314
|
});
|
|
1802
2315
|
ws.addEventListener('error', () => {});
|
|
1803
|
-
ws.addEventListener('message', e => {
|
|
2316
|
+
ws.addEventListener('message', (e) => {
|
|
1804
2317
|
try {
|
|
1805
2318
|
const m = JSON.parse(e.data);
|
|
1806
2319
|
if (m.type === 'snapshot' && m.state) {
|
|
@@ -1808,13 +2321,18 @@ function App() {
|
|
|
1808
2321
|
} else if (m.type === 'selected') {
|
|
1809
2322
|
setSelected(m.selected);
|
|
1810
2323
|
} else if (m.type === 'comments' && typeof m.file === 'string') {
|
|
1811
|
-
setCommentsByFile(prev => ({ ...prev, [m.file]: m.comments || [] }));
|
|
2324
|
+
setCommentsByFile((prev) => ({ ...prev, [m.file]: m.comments || [] }));
|
|
1812
2325
|
} else if (m.type === 'ai-activity' && typeof m.file === 'string') {
|
|
1813
2326
|
// Phase 8 Task 4 — relay to every open iframe; each canvas's
|
|
1814
2327
|
// AiBanner filters by its own file path. Lightweight broadcast
|
|
1815
2328
|
// (one envelope per change, not per iframe count).
|
|
1816
2329
|
for (const el of iframesRef.current.values()) {
|
|
1817
|
-
try {
|
|
2330
|
+
try {
|
|
2331
|
+
el.contentWindow.postMessage(
|
|
2332
|
+
{ dgn: 'ai-activity', file: m.file, entry: m.entry },
|
|
2333
|
+
'*'
|
|
2334
|
+
);
|
|
2335
|
+
} catch {}
|
|
1818
2336
|
}
|
|
1819
2337
|
} else if (m.type === 'sync:status' && m.payload) {
|
|
1820
2338
|
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
@@ -1827,7 +2345,9 @@ function App() {
|
|
|
1827
2345
|
// Also relay to iframes so canvas-level "Reload?" UI (if any)
|
|
1828
2346
|
// can react. Outer banner is the primary prompt.
|
|
1829
2347
|
for (const el of iframesRef.current.values()) {
|
|
1830
|
-
try {
|
|
2348
|
+
try {
|
|
2349
|
+
el.contentWindow.postMessage({ dgn: 'git-lifecycle', payload: m.payload }, '*');
|
|
2350
|
+
} catch {}
|
|
1831
2351
|
}
|
|
1832
2352
|
}
|
|
1833
2353
|
} catch {}
|
|
@@ -1839,7 +2359,9 @@ function App() {
|
|
|
1839
2359
|
|
|
1840
2360
|
function wsSend(obj) {
|
|
1841
2361
|
const ws = wsRef.current;
|
|
1842
|
-
try {
|
|
2362
|
+
try {
|
|
2363
|
+
if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));
|
|
2364
|
+
} catch {}
|
|
1843
2365
|
}
|
|
1844
2366
|
|
|
1845
2367
|
// ----- Tab management (single-canvas) -----
|
|
@@ -1848,7 +2370,7 @@ function App() {
|
|
|
1848
2370
|
// (iframesRef, comments push, WS `tabs` message) doesn't need refactoring.
|
|
1849
2371
|
// ARTBOARDS slot in the menubar reads `tabs.length` and reports 0 or 1.
|
|
1850
2372
|
const openTab = useCallback((path) => {
|
|
1851
|
-
setTabs(prev => {
|
|
2373
|
+
setTabs((prev) => {
|
|
1852
2374
|
// Drop the previously-open iframe so we don't leak DOM nodes.
|
|
1853
2375
|
for (const t of prev) if (t.path !== path) iframesRef.current.delete(t.path);
|
|
1854
2376
|
return [{ path }];
|
|
@@ -1857,18 +2379,21 @@ function App() {
|
|
|
1857
2379
|
setFocusedCommentId(null);
|
|
1858
2380
|
}, []);
|
|
1859
2381
|
|
|
1860
|
-
const openSystem = useCallback(
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2382
|
+
const openSystem = useCallback(
|
|
2383
|
+
(dsName) => {
|
|
2384
|
+
// DsFolderRow passes the clicked DS name → scope the System view to it so
|
|
2385
|
+
// each folder shows its own tokens + previews. The no-arg callers (menubar,
|
|
2386
|
+
// keyboard reopen) only load default data on first open.
|
|
2387
|
+
const ds = typeof dsName === 'string' ? dsName : undefined;
|
|
2388
|
+
if (ds) loadSystemData(ds);
|
|
2389
|
+
else if (!systemData) loadSystemData();
|
|
2390
|
+
openTab(SYSTEM_TAB);
|
|
2391
|
+
},
|
|
2392
|
+
[systemData, loadSystemData, openTab]
|
|
2393
|
+
);
|
|
1869
2394
|
|
|
1870
2395
|
useEffect(() => {
|
|
1871
|
-
wsSend({ type: 'tabs', tabs: tabs.map(t => t.path).filter(p => p !== SYSTEM_TAB) });
|
|
2396
|
+
wsSend({ type: 'tabs', tabs: tabs.map((t) => t.path).filter((p) => p !== SYSTEM_TAB) });
|
|
1872
2397
|
}, [tabs]);
|
|
1873
2398
|
|
|
1874
2399
|
useEffect(() => {
|
|
@@ -1877,19 +2402,22 @@ function App() {
|
|
|
1877
2402
|
else wsSend({ type: 'active', file: '' });
|
|
1878
2403
|
}, [activePath]);
|
|
1879
2404
|
|
|
1880
|
-
const closeTab = useCallback(
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
if (
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2405
|
+
const closeTab = useCallback(
|
|
2406
|
+
(path) => {
|
|
2407
|
+
setTabs((prev) => {
|
|
2408
|
+
const idx = prev.findIndex((t) => t.path === path);
|
|
2409
|
+
if (idx < 0) return prev;
|
|
2410
|
+
const next = prev.filter((t) => t.path !== path);
|
|
2411
|
+
if (path === activePath) {
|
|
2412
|
+
if (next.length === 0) setActivePath(null);
|
|
2413
|
+
else setActivePath(next[Math.max(0, idx - 1)].path);
|
|
2414
|
+
}
|
|
2415
|
+
return next;
|
|
2416
|
+
});
|
|
2417
|
+
iframesRef.current.delete(path);
|
|
2418
|
+
},
|
|
2419
|
+
[activePath]
|
|
2420
|
+
);
|
|
1893
2421
|
|
|
1894
2422
|
const reloadActive = useCallback(() => {
|
|
1895
2423
|
if (!activePath || activePath === SYSTEM_TAB) {
|
|
@@ -1908,7 +2436,9 @@ function App() {
|
|
|
1908
2436
|
if (activePath && activePath !== SYSTEM_TAB) {
|
|
1909
2437
|
const el = iframesRef.current.get(activePath);
|
|
1910
2438
|
if (el && el.contentWindow) {
|
|
1911
|
-
try {
|
|
2439
|
+
try {
|
|
2440
|
+
el.contentWindow.postMessage({ dgn: 'force-clear' }, '*');
|
|
2441
|
+
} catch {}
|
|
1912
2442
|
}
|
|
1913
2443
|
}
|
|
1914
2444
|
}, [activePath]);
|
|
@@ -1919,7 +2449,9 @@ function App() {
|
|
|
1919
2449
|
const el = iframesRef.current.get(activePath);
|
|
1920
2450
|
if (!el || !el.contentWindow) return;
|
|
1921
2451
|
const list = commentsByFile[activePath] || [];
|
|
1922
|
-
try {
|
|
2452
|
+
try {
|
|
2453
|
+
el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*');
|
|
2454
|
+
} catch {}
|
|
1923
2455
|
}, [activePath, commentsByFile]);
|
|
1924
2456
|
|
|
1925
2457
|
// ----- Inbound messages from iframes -----
|
|
@@ -1935,6 +2467,29 @@ function App() {
|
|
|
1935
2467
|
if (e.origin !== expectedOrigin) return;
|
|
1936
2468
|
const m = e.data;
|
|
1937
2469
|
if (!m || typeof m !== 'object' || !m.dgn) return;
|
|
2470
|
+
if (m.dgn === 'tool-cursor') {
|
|
2471
|
+
// Phase 24 — show the active canvas tool's cursor across the WHOLE app
|
|
2472
|
+
// shell (sidebar, top bar, everything) so the custom cursor is visible
|
|
2473
|
+
// everywhere in maude, not just inside the canvas iframe. The canvas
|
|
2474
|
+
// sends only a tool TOKEN; we resolve it to a cursor string from our own
|
|
2475
|
+
// trusted map (resolveToolCursor) and apply THAT — never a raw
|
|
2476
|
+
// canvas-supplied value. A malicious synced canvas (DDR-054) can thus
|
|
2477
|
+
// only pick a known, always-visible glyph; it cannot inject an
|
|
2478
|
+
// invisible/displaced SVG cursor as a clickjacking aid over the un-CSP'd
|
|
2479
|
+
// shell (phase-24 ethical-hacker Finding 2; DDR-067).
|
|
2480
|
+
const cursor = resolveToolCursor(m.tool);
|
|
2481
|
+
if (cursor) {
|
|
2482
|
+
document.body.style.cursor = cursor;
|
|
2483
|
+
let el = document.getElementById('dc-app-cursor');
|
|
2484
|
+
if (!el) {
|
|
2485
|
+
el = document.createElement('style');
|
|
2486
|
+
el.id = 'dc-app-cursor';
|
|
2487
|
+
document.head.appendChild(el);
|
|
2488
|
+
}
|
|
2489
|
+
el.textContent = `* { cursor: ${cursor} !important; }`;
|
|
2490
|
+
}
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
1938
2493
|
if (m.dgn === 'select' && m.selection) {
|
|
1939
2494
|
wsSend({ type: 'select', selection: m.selection });
|
|
1940
2495
|
setSelected(m.selection);
|
|
@@ -1995,7 +2550,7 @@ function App() {
|
|
|
1995
2550
|
wsSend({ type: 'comments-patch', id: m.id, patch: m.patch });
|
|
1996
2551
|
} else if (m.dgn === 'comment-delete' && m.id) {
|
|
1997
2552
|
wsSend({ type: 'comments-delete', id: m.id });
|
|
1998
|
-
setFocusedCommentId(prev => (prev === m.id ? null : prev));
|
|
2553
|
+
setFocusedCommentId((prev) => (prev === m.id ? null : prev));
|
|
1999
2554
|
} else if (m.dgn === 'comment-click' && m.id) {
|
|
2000
2555
|
setFocusedCommentId(m.id);
|
|
2001
2556
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
@@ -2003,17 +2558,87 @@ function App() {
|
|
|
2003
2558
|
const list = commentsByFile[m.file] || [];
|
|
2004
2559
|
const el = [...iframesRef.current.entries()].find(([k]) => k === m.file)?.[1];
|
|
2005
2560
|
if (el && el.contentWindow) {
|
|
2006
|
-
try {
|
|
2561
|
+
try {
|
|
2562
|
+
el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*');
|
|
2563
|
+
} catch {}
|
|
2007
2564
|
// System-review D9 — seed the just-loaded canvas with the current
|
|
2008
2565
|
// chrome theme so a canvas opened AFTER a theme toggle starts
|
|
2009
2566
|
// correct (no flash from the dark default).
|
|
2010
|
-
try {
|
|
2011
|
-
|
|
2012
|
-
|
|
2567
|
+
try {
|
|
2568
|
+
el.contentWindow.postMessage({ dgn: 'theme', theme }, '*');
|
|
2569
|
+
} catch {}
|
|
2570
|
+
if (focusedCommentId && list.some((c) => c.id === focusedCommentId)) {
|
|
2571
|
+
try {
|
|
2572
|
+
el.contentWindow.postMessage({ dgn: 'comment-focus', id: focusedCommentId }, '*');
|
|
2573
|
+
} catch {}
|
|
2013
2574
|
}
|
|
2014
2575
|
}
|
|
2576
|
+
} else if (m.dgn === 'export-request' && m.id && m.payload) {
|
|
2577
|
+
// The export dialog renders inside the canvas iframe (canvas origin),
|
|
2578
|
+
// but /_api/export is a privileged MAIN-origin endpoint deliberately
|
|
2579
|
+
// kept off the canvas allowlist (DDR-060). A direct in-iframe fetch
|
|
2580
|
+
// therefore 403s ("Forbidden (canvas origin)"). Bridge it: run the
|
|
2581
|
+
// export here on the trusted main origin, stream the download, and
|
|
2582
|
+
// report status back to the iframe. Origin is already validated
|
|
2583
|
+
// (e.origin === expectedOrigin) above, so only the real canvas iframe
|
|
2584
|
+
// can ask — this is NOT a generic fetch proxy.
|
|
2585
|
+
void runBridgedExport(e.source, m.id, m.payload);
|
|
2586
|
+
} else if (m.dgn === 'export-history-request' && m.id) {
|
|
2587
|
+
// Same bridge for the dialog's Recent tab (/_api/export-history is
|
|
2588
|
+
// also main-origin-only).
|
|
2589
|
+
void runBridgedHistory(e.source, m.id);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
// Reply target for the export bridge: the canvas iframe's own origin.
|
|
2593
|
+
const replyOrigin = cfg?.canvasOrigin || window.location.origin;
|
|
2594
|
+
async function runBridgedExport(source, id, payload) {
|
|
2595
|
+
const reply = (msg) => {
|
|
2596
|
+
try {
|
|
2597
|
+
if (source) source.postMessage({ dgn: 'export-result', id, ...msg }, replyOrigin);
|
|
2598
|
+
} catch {}
|
|
2599
|
+
};
|
|
2600
|
+
try {
|
|
2601
|
+
const r = await fetch('/_api/export', {
|
|
2602
|
+
method: 'POST',
|
|
2603
|
+
headers: { 'content-type': 'application/json' },
|
|
2604
|
+
body: JSON.stringify(payload),
|
|
2605
|
+
});
|
|
2606
|
+
if (!r.ok) {
|
|
2607
|
+
reply({ ok: false, error: (await r.text()) || String(r.status) });
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const disp = r.headers.get('Content-Disposition') || '';
|
|
2611
|
+
const fn = /filename="([^"]+)"/.exec(disp);
|
|
2612
|
+
const filename = (fn && fn[1]) || 'export';
|
|
2613
|
+
const blob = await r.blob();
|
|
2614
|
+
const url = URL.createObjectURL(blob);
|
|
2615
|
+
const a = document.createElement('a');
|
|
2616
|
+
a.href = url;
|
|
2617
|
+
a.download = filename;
|
|
2618
|
+
document.body.appendChild(a);
|
|
2619
|
+
a.click();
|
|
2620
|
+
a.remove();
|
|
2621
|
+
URL.revokeObjectURL(url);
|
|
2622
|
+
reply({ ok: true, filename });
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
reply({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
2015
2625
|
}
|
|
2016
2626
|
}
|
|
2627
|
+
async function runBridgedHistory(source, id) {
|
|
2628
|
+
let history = [];
|
|
2629
|
+
try {
|
|
2630
|
+
const r = await fetch('/_api/export-history');
|
|
2631
|
+
if (r.ok) {
|
|
2632
|
+
const data = await r.json();
|
|
2633
|
+
if (Array.isArray(data.history)) history = data.history;
|
|
2634
|
+
}
|
|
2635
|
+
} catch {
|
|
2636
|
+
/* best-effort — empty list */
|
|
2637
|
+
}
|
|
2638
|
+
try {
|
|
2639
|
+
if (source) source.postMessage({ dgn: 'export-history-result', id, history }, replyOrigin);
|
|
2640
|
+
} catch {}
|
|
2641
|
+
}
|
|
2017
2642
|
window.addEventListener('message', onMessage);
|
|
2018
2643
|
return () => window.removeEventListener('message', onMessage);
|
|
2019
2644
|
}, [commentsByFile, focusedCommentId, cfg, theme]);
|
|
@@ -2026,7 +2651,9 @@ function App() {
|
|
|
2026
2651
|
if (!activePath || activePath === SYSTEM_TAB) return;
|
|
2027
2652
|
const el = iframesRef.current.get(activePath);
|
|
2028
2653
|
if (el && el.contentWindow) {
|
|
2029
|
-
try {
|
|
2654
|
+
try {
|
|
2655
|
+
el.contentWindow.postMessage({ dgn: 'force-clear' }, '*');
|
|
2656
|
+
} catch {}
|
|
2030
2657
|
}
|
|
2031
2658
|
}, [activePath]);
|
|
2032
2659
|
|
|
@@ -2038,37 +2665,44 @@ function App() {
|
|
|
2038
2665
|
}, []);
|
|
2039
2666
|
const deleteComment = useCallback((id) => {
|
|
2040
2667
|
wsSend({ type: 'comments-delete', id });
|
|
2041
|
-
setFocusedCommentId(prev => (prev === id ? null : prev));
|
|
2668
|
+
setFocusedCommentId((prev) => (prev === id ? null : prev));
|
|
2042
2669
|
}, []);
|
|
2043
2670
|
|
|
2044
2671
|
// Jump from right-sidebar list to a comment: open file tab if needed, focus pin.
|
|
2045
2672
|
// The iframe may be freshly mounted; the loaded handler also re-sends focus if focusedCommentId matches.
|
|
2046
|
-
const jumpToComment = useCallback(
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
if (id == null) {
|
|
2052
|
-
setFocusedCommentId(null);
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
setFocusedCommentId(id);
|
|
2056
|
-
// Try sending focus immediately (existing iframe) and again after a short delay (newly opened tab).
|
|
2057
|
-
const send = () => {
|
|
2058
|
-
const el = iframesRef.current.get(file);
|
|
2059
|
-
if (el && el.contentWindow) {
|
|
2060
|
-
try { el.contentWindow.postMessage({ dgn: 'comment-focus', id }, '*'); } catch {}
|
|
2673
|
+
const jumpToComment = useCallback(
|
|
2674
|
+
(file, id) => {
|
|
2675
|
+
if (file && file !== activePath) {
|
|
2676
|
+
setTabs((prev) => (prev.find((t) => t.path === file) ? prev : [...prev, { path: file }]));
|
|
2677
|
+
setActivePath(file);
|
|
2061
2678
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2679
|
+
if (id == null) {
|
|
2680
|
+
setFocusedCommentId(null);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
setFocusedCommentId(id);
|
|
2684
|
+
// Try sending focus immediately (existing iframe) and again after a short delay (newly opened tab).
|
|
2685
|
+
const send = () => {
|
|
2686
|
+
const el = iframesRef.current.get(file);
|
|
2687
|
+
if (el && el.contentWindow) {
|
|
2688
|
+
try {
|
|
2689
|
+
el.contentWindow.postMessage({ dgn: 'comment-focus', id }, '*');
|
|
2690
|
+
} catch {}
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
send();
|
|
2694
|
+
setTimeout(send, 200);
|
|
2695
|
+
},
|
|
2696
|
+
[activePath]
|
|
2697
|
+
);
|
|
2066
2698
|
|
|
2067
2699
|
// ----- Keyboard shortcuts (no Cmd+W — let browser close the tab) -----
|
|
2068
2700
|
useEffect(() => {
|
|
2069
2701
|
function onKey(e) {
|
|
2070
2702
|
const meta = e.metaKey || e.ctrlKey;
|
|
2071
|
-
const inEditable =
|
|
2703
|
+
const inEditable =
|
|
2704
|
+
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) ||
|
|
2705
|
+
document.activeElement?.isContentEditable;
|
|
2072
2706
|
// Phase 4.1: shell-side letter shortcuts (H/T/S) must not double-fire
|
|
2073
2707
|
// inside a focused canvas iframe — the canvas input router owns those
|
|
2074
2708
|
// letters as tool-mode keys (V/H/C). Cmd-modified shortcuts (⌘R, ⌘⇧M,
|
|
@@ -2084,7 +2718,7 @@ function App() {
|
|
|
2084
2718
|
// Cmd+Shift+M / Ctrl+Shift+M — toggle right "Comments" panel
|
|
2085
2719
|
if (meta && e.shiftKey && (e.key === 'm' || e.key === 'M')) {
|
|
2086
2720
|
e.preventDefault();
|
|
2087
|
-
setCommentsPanelOpen(v => !v);
|
|
2721
|
+
setCommentsPanelOpen((v) => !v);
|
|
2088
2722
|
return;
|
|
2089
2723
|
}
|
|
2090
2724
|
// Cmd+C / Ctrl+C — Phase 4.1 removed the shell-side comment-drop chord.
|
|
@@ -2092,8 +2726,18 @@ function App() {
|
|
|
2092
2726
|
// then click the element) or right-click "Add comment". Cmd+C now
|
|
2093
2727
|
// reverts to native browser copy.
|
|
2094
2728
|
if (meta && !e.shiftKey && !e.altKey && (e.key === 'c' || e.key === 'C')) {
|
|
2095
|
-
if (
|
|
2096
|
-
|
|
2729
|
+
if (
|
|
2730
|
+
selected &&
|
|
2731
|
+
selected.selector &&
|
|
2732
|
+
activePath &&
|
|
2733
|
+
activePath !== SYSTEM_TAB &&
|
|
2734
|
+
!inEditable &&
|
|
2735
|
+
console &&
|
|
2736
|
+
console.warn
|
|
2737
|
+
) {
|
|
2738
|
+
console.warn(
|
|
2739
|
+
'Cmd+C comment-drop deprecated — press C inside the canvas to enter Comment tool, then click the element.'
|
|
2740
|
+
);
|
|
2097
2741
|
}
|
|
2098
2742
|
// Fall through to native copy.
|
|
2099
2743
|
}
|
|
@@ -2126,14 +2770,14 @@ function App() {
|
|
|
2126
2770
|
if (e.key === 't' || e.key === 'T') {
|
|
2127
2771
|
if (e.shiftKey || meta) return;
|
|
2128
2772
|
e.preventDefault();
|
|
2129
|
-
setSidebarOpen(v => !v);
|
|
2773
|
+
setSidebarOpen((v) => !v);
|
|
2130
2774
|
return;
|
|
2131
2775
|
}
|
|
2132
2776
|
// H — toggle show-hidden (sidecars + project/runtime orphans)
|
|
2133
2777
|
if (e.key === 'h' || e.key === 'H') {
|
|
2134
2778
|
if (e.shiftKey || meta) return;
|
|
2135
2779
|
e.preventDefault();
|
|
2136
|
-
setShowHidden(v => !v);
|
|
2780
|
+
setShowHidden((v) => !v);
|
|
2137
2781
|
return;
|
|
2138
2782
|
}
|
|
2139
2783
|
// S — toggle Design system view
|
|
@@ -2155,18 +2799,31 @@ function App() {
|
|
|
2155
2799
|
// Esc — clear focused pin. The in-place composer (Phase 6) and thread
|
|
2156
2800
|
// popover handle their own Esc inside the iframe.
|
|
2157
2801
|
if (e.key === 'Escape') {
|
|
2158
|
-
if (focusedCommentId) {
|
|
2802
|
+
if (focusedCommentId) {
|
|
2803
|
+
setFocusedCommentId(null);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2159
2806
|
}
|
|
2160
2807
|
}
|
|
2161
2808
|
window.addEventListener('keydown', onKey);
|
|
2162
2809
|
return () => window.removeEventListener('keydown', onKey);
|
|
2163
|
-
}, [
|
|
2810
|
+
}, [
|
|
2811
|
+
reloadActive,
|
|
2812
|
+
selected,
|
|
2813
|
+
activePath,
|
|
2814
|
+
focusedCommentId,
|
|
2815
|
+
sidebarOpen,
|
|
2816
|
+
openSystem,
|
|
2817
|
+
closeTab,
|
|
2818
|
+
clearActiveCanvasSelection,
|
|
2819
|
+
]);
|
|
2164
2820
|
|
|
2165
2821
|
const registerIframe = useCallback((path, el) => {
|
|
2166
2822
|
if (el) iframesRef.current.set(path, el);
|
|
2167
2823
|
}, []);
|
|
2168
2824
|
|
|
2169
|
-
const activeFileComments =
|
|
2825
|
+
const activeFileComments =
|
|
2826
|
+
activePath && activePath !== SYSTEM_TAB ? commentsByFile[activePath] || [] : [];
|
|
2170
2827
|
const totalOpen = totalCounts(commentsByFile).open;
|
|
2171
2828
|
|
|
2172
2829
|
// Suppress the native browser context menu across the shell — the canvas
|
|
@@ -2177,7 +2834,7 @@ function App() {
|
|
|
2177
2834
|
// copy/paste still works.
|
|
2178
2835
|
const onShellContextMenu = useCallback((e) => {
|
|
2179
2836
|
const t = e.target;
|
|
2180
|
-
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
|
|
2837
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) {
|
|
2181
2838
|
return;
|
|
2182
2839
|
}
|
|
2183
2840
|
e.preventDefault();
|
|
@@ -2185,7 +2842,9 @@ function App() {
|
|
|
2185
2842
|
|
|
2186
2843
|
return (
|
|
2187
2844
|
<div
|
|
2188
|
-
className={
|
|
2845
|
+
className={
|
|
2846
|
+
'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')
|
|
2847
|
+
}
|
|
2189
2848
|
onContextMenu={onShellContextMenu}
|
|
2190
2849
|
>
|
|
2191
2850
|
<SyncBanner status={syncStatus} />
|
|
@@ -2214,7 +2873,11 @@ function App() {
|
|
|
2214
2873
|
<span>Repo state changed — reload to sync?</span>
|
|
2215
2874
|
<button
|
|
2216
2875
|
type="button"
|
|
2217
|
-
onClick={() => {
|
|
2876
|
+
onClick={() => {
|
|
2877
|
+
try {
|
|
2878
|
+
window.location.reload();
|
|
2879
|
+
} catch {}
|
|
2880
|
+
}}
|
|
2218
2881
|
style={{
|
|
2219
2882
|
padding: '3px 10px',
|
|
2220
2883
|
background: '#2563eb',
|
|
@@ -2224,7 +2887,9 @@ function App() {
|
|
|
2224
2887
|
font: '500 11px/1.2 system-ui',
|
|
2225
2888
|
cursor: 'pointer',
|
|
2226
2889
|
}}
|
|
2227
|
-
>
|
|
2890
|
+
>
|
|
2891
|
+
Reload
|
|
2892
|
+
</button>
|
|
2228
2893
|
<button
|
|
2229
2894
|
type="button"
|
|
2230
2895
|
onClick={() => setGitLifecycle(null)}
|
|
@@ -2237,7 +2902,9 @@ function App() {
|
|
|
2237
2902
|
font: '500 11px/1.2 system-ui',
|
|
2238
2903
|
cursor: 'pointer',
|
|
2239
2904
|
}}
|
|
2240
|
-
>
|
|
2905
|
+
>
|
|
2906
|
+
Dismiss
|
|
2907
|
+
</button>
|
|
2241
2908
|
</div>
|
|
2242
2909
|
)}
|
|
2243
2910
|
<Sidebar
|
|
@@ -2262,12 +2929,12 @@ function App() {
|
|
|
2262
2929
|
openMenu={openMenu}
|
|
2263
2930
|
setOpenMenu={setOpenMenu}
|
|
2264
2931
|
commentsPanelOpen={commentsPanelOpen}
|
|
2265
|
-
onToggleComments={() => setCommentsPanelOpen(v => !v)}
|
|
2932
|
+
onToggleComments={() => setCommentsPanelOpen((v) => !v)}
|
|
2266
2933
|
onOpenSystem={openSystem}
|
|
2267
2934
|
sidebarOpen={sidebarOpen}
|
|
2268
|
-
onToggleSidebar={() => setSidebarOpen(v => !v)}
|
|
2935
|
+
onToggleSidebar={() => setSidebarOpen((v) => !v)}
|
|
2269
2936
|
showHidden={showHidden}
|
|
2270
|
-
onToggleShowHidden={() => setShowHidden(v => !v)}
|
|
2937
|
+
onToggleShowHidden={() => setShowHidden((v) => !v)}
|
|
2271
2938
|
onOpenHelp={() => setHelpOpen(true)}
|
|
2272
2939
|
annotationsVisible={annotationsVisible}
|
|
2273
2940
|
onToggleAnnotations={toggleAnnotations}
|