@1agh/maude 0.52.2 → 0.53.1
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/bin/_import-asset.mjs +1 -6
- package/apps/studio/bin/_import-brand.mjs +1 -1
- package/apps/studio/bin/_video-playwright.mjs +5 -1
- package/apps/studio/canvas-build.ts +91 -0
- package/apps/studio/canvas-lib.tsx +12 -1
- package/apps/studio/canvas-shell.tsx +60 -11
- package/apps/studio/client/app.jsx +183 -98
- package/apps/studio/client/canvas-url.js +6 -0
- package/apps/studio/client/styles/3-shell-maude.css +7 -0
- package/apps/studio/comments-overlay.tsx +59 -51
- package/apps/studio/dist/client.bundle.js +1266 -1266
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +105 -4
- package/apps/studio/read-only-mode.ts +36 -0
- package/apps/studio/test/canvas-url.test.ts +13 -0
- package/apps/studio/test/read-only-gate.test.ts +130 -0
- package/apps/studio/test/report-proxy.test.ts +125 -0
- package/apps/studio/test/use-tool-mode.test.tsx +23 -0
- package/apps/studio/tool-palette.tsx +65 -51
- package/apps/studio/use-keyboard-discipline.tsx +6 -0
- package/apps/studio/use-tool-mode.tsx +37 -7
- package/apps/studio/whats-new.json +28 -1
- package/cli/bin/maude.mjs +0 -1
- package/package.json +8 -8
- package/cli/commands/share.mjs +0 -188
- package/cli/lib/share-plan.mjs +0 -100
- package/cli/lib/share-plan.test.mjs +0 -66
|
@@ -325,7 +325,7 @@ function shellToast(message, ok = false, action) {
|
|
|
325
325
|
// pressed. Surface that ONCE, gated by a localStorage marker (same family as
|
|
326
326
|
// `mdcc-whatsnew-seen`). Auto-dismisses; a V press (learned) clears it early.
|
|
327
327
|
const BROWSE_HINT_SEEN = 'maude-browse-hint-seen';
|
|
328
|
-
function browseFirstRunHint() {
|
|
328
|
+
function browseFirstRunHint(readOnly = false) {
|
|
329
329
|
if (typeof document === 'undefined' || typeof localStorage === 'undefined') return;
|
|
330
330
|
try {
|
|
331
331
|
if (localStorage.getItem(BROWSE_HINT_SEEN) === '1') return;
|
|
@@ -346,7 +346,9 @@ function browseFirstRunHint() {
|
|
|
346
346
|
'<span>Your mock is <strong>live</strong> — click things to try it. ' +
|
|
347
347
|
'Press <kbd style="padding:1px 6px;border-radius:5px;border:1px solid var(--border-1,#333a45);' +
|
|
348
348
|
'background:var(--surface-3,#262b33);font-family:var(--font-mono,monospace)">V</kbd> ' +
|
|
349
|
-
|
|
349
|
+
// Cloud Phase 25 C2 — a viewer selects to inspect, never to edit; the
|
|
350
|
+
// hint must not promise an editor the role doesn't have.
|
|
351
|
+
(readOnly ? 'to select & inspect.</span>' : 'to select & edit like Figma.</span>') +
|
|
350
352
|
'<button type="button" aria-label="Dismiss" style="background:none;border:none;color:inherit;' +
|
|
351
353
|
'cursor:pointer;font-size:15px;line-height:1;opacity:.65;padding:2px">×</button>';
|
|
352
354
|
const dismiss = () => {
|
|
@@ -2424,6 +2426,9 @@ function sectionMetaFor(g) {
|
|
|
2424
2426
|
}
|
|
2425
2427
|
|
|
2426
2428
|
function Sidebar({
|
|
2429
|
+
// Cloud Phase 25 C2 — viewer role: create / delete / move / rename
|
|
2430
|
+
// affordances are absent (buttons, composer, row menus, drag & drop).
|
|
2431
|
+
readOnly = false,
|
|
2427
2432
|
groups,
|
|
2428
2433
|
activePath,
|
|
2429
2434
|
activeDsName,
|
|
@@ -2588,21 +2593,23 @@ function Sidebar({
|
|
|
2588
2593
|
<div className="st-sb-hd">
|
|
2589
2594
|
<span className="st-sb-title">Files</span>
|
|
2590
2595
|
<div className="st-sb-hd-actions">
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2596
|
+
{!readOnly && (
|
|
2597
|
+
<button
|
|
2598
|
+
type="button"
|
|
2599
|
+
className="st-iconbtn"
|
|
2600
|
+
data-tip="New blank brief board"
|
|
2601
|
+
aria-label="New blank brief board"
|
|
2602
|
+
aria-expanded={creating && composerMode === 'board'}
|
|
2603
|
+
onClick={() => {
|
|
2604
|
+
setNewErr('');
|
|
2605
|
+
setComposerMode('board');
|
|
2606
|
+
setCreating((v) => (composerMode === 'board' ? !v : true));
|
|
2607
|
+
}}
|
|
2608
|
+
>
|
|
2609
|
+
<StIcon name="plus" size={15} />
|
|
2610
|
+
</button>
|
|
2611
|
+
)}
|
|
2612
|
+
{!readOnly && defaultFolderParent && (
|
|
2606
2613
|
<button
|
|
2607
2614
|
type="button"
|
|
2608
2615
|
className="st-iconbtn"
|
|
@@ -2758,7 +2765,7 @@ function Sidebar({
|
|
|
2758
2765
|
// root". Without this a canvas inside a folder could never return
|
|
2759
2766
|
// to the top level via drag & drop — DirRow only covers actual
|
|
2760
2767
|
// subfolders, never the group root itself.
|
|
2761
|
-
const canDropOnRoot = !isDs && g.kind === 'canvas';
|
|
2768
|
+
const canDropOnRoot = !isDs && g.kind === 'canvas' && !readOnly;
|
|
2762
2769
|
const rootDropHandlers = canDropOnRoot ? treeDrag.dropProps(g.fullPath, true) : {};
|
|
2763
2770
|
const isRootOver = treeDrag.overDir === g.fullPath;
|
|
2764
2771
|
return (
|
|
@@ -2790,12 +2797,12 @@ function Sidebar({
|
|
|
2790
2797
|
dsFolders={g.dsFolders}
|
|
2791
2798
|
activeDsName={activeDsName}
|
|
2792
2799
|
onOpenSystem={isDs ? onOpenSystem : undefined}
|
|
2793
|
-
onDelete={isDs ? undefined : onDeleteBoard}
|
|
2800
|
+
onDelete={isDs || readOnly ? undefined : onDeleteBoard}
|
|
2794
2801
|
dirtyByPath={dirtyByPath}
|
|
2795
2802
|
canvasKinds={canvasKinds}
|
|
2796
2803
|
dirPath={g.fullPath}
|
|
2797
|
-
drag={!isDs && g.kind === 'canvas' ? treeDrag : undefined}
|
|
2798
|
-
menu={!isDs && g.kind === 'canvas' ? rowMenu : undefined}
|
|
2804
|
+
drag={!isDs && g.kind === 'canvas' && !readOnly ? treeDrag : undefined}
|
|
2805
|
+
menu={!isDs && g.kind === 'canvas' && !readOnly ? rowMenu : undefined}
|
|
2799
2806
|
/>
|
|
2800
2807
|
) : (
|
|
2801
2808
|
<div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
@@ -3460,7 +3467,7 @@ function HelpDropdown({ onAction, onClose }) {
|
|
|
3460
3467
|
);
|
|
3461
3468
|
}
|
|
3462
3469
|
|
|
3463
|
-
function SelectionDropdown({ onAction, onClose }) {
|
|
3470
|
+
function SelectionDropdown({ onAction, onClose, readOnly = false }) {
|
|
3464
3471
|
return (
|
|
3465
3472
|
<DropdownMenu
|
|
3466
3473
|
label="Selection"
|
|
@@ -3469,16 +3476,40 @@ function SelectionDropdown({ onAction, onClose }) {
|
|
|
3469
3476
|
onClose={onClose}
|
|
3470
3477
|
items={[
|
|
3471
3478
|
{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' },
|
|
3472
|
-
|
|
3479
|
+
// Cloud Phase 25 C2 — annotation selection exists to edit/delete
|
|
3480
|
+
// annotations; absent for a viewer.
|
|
3481
|
+
...(readOnly
|
|
3482
|
+
? []
|
|
3483
|
+
: [{ id: 'select-all-annotations', label: 'Select all annotations', shortcut: '⌘ ⇧ A' }]),
|
|
3473
3484
|
]}
|
|
3474
3485
|
/>
|
|
3475
3486
|
);
|
|
3476
3487
|
}
|
|
3477
3488
|
|
|
3478
|
-
function ToolsDropdown({ onAction, onClose }) {
|
|
3489
|
+
function ToolsDropdown({ onAction, onClose, readOnly = false }) {
|
|
3479
3490
|
// Mirrors DEFAULT_TOOLS in apps/studio/use-tool-mode.tsx — kept in sync by
|
|
3480
3491
|
// hand because the menubar lives in the dev-server shell (no shared bundle
|
|
3481
|
-
// with the canvas iframes).
|
|
3492
|
+
// with the canvas iframes). Cloud Phase 25 C2 — a viewer keeps only the
|
|
3493
|
+
// navigate/inspect tools (same READ_ONLY_TOOL_IDS the canvas enforces).
|
|
3494
|
+
const items = [
|
|
3495
|
+
// feature-4 (browse/move split) — Browse is the boot default (mock is
|
|
3496
|
+
// alive); Move (V) is the select tool.
|
|
3497
|
+
{ id: 'browse', label: 'Browse (interact)', shortcut: '' },
|
|
3498
|
+
{ id: 'move', label: 'Select', shortcut: 'V' },
|
|
3499
|
+
{ id: 'hand', label: 'Hand', shortcut: 'H' },
|
|
3500
|
+
...(readOnly
|
|
3501
|
+
? []
|
|
3502
|
+
: [
|
|
3503
|
+
{ id: 'comment', label: 'Comment', shortcut: 'C' },
|
|
3504
|
+
{ id: 'pen', label: 'Pen', shortcut: 'B' },
|
|
3505
|
+
{ id: 'rect', label: 'Rect', shortcut: 'R' },
|
|
3506
|
+
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O' },
|
|
3507
|
+
{ id: 'sticky', label: 'Sticky', shortcut: 'N' },
|
|
3508
|
+
{ id: 'arrow', label: 'Arrow', shortcut: 'A' },
|
|
3509
|
+
{ id: 'text', label: 'Text', shortcut: 'T' },
|
|
3510
|
+
{ id: 'eraser', label: 'Eraser', shortcut: 'E' },
|
|
3511
|
+
]),
|
|
3512
|
+
];
|
|
3482
3513
|
return (
|
|
3483
3514
|
<DropdownMenu
|
|
3484
3515
|
label="Tools"
|
|
@@ -3486,35 +3517,25 @@ function ToolsDropdown({ onAction, onClose }) {
|
|
|
3486
3517
|
header="Tool palette"
|
|
3487
3518
|
onAction={onAction}
|
|
3488
3519
|
onClose={onClose}
|
|
3489
|
-
items={
|
|
3490
|
-
// feature-4 (browse/move split) — Browse is the boot default (mock is
|
|
3491
|
-
// alive); Move (V) is the select tool.
|
|
3492
|
-
{ id: 'browse', label: 'Browse (interact)', shortcut: '' },
|
|
3493
|
-
{ id: 'move', label: 'Select', shortcut: 'V' },
|
|
3494
|
-
{ id: 'hand', label: 'Hand', shortcut: 'H' },
|
|
3495
|
-
{ id: 'comment', label: 'Comment', shortcut: 'C' },
|
|
3496
|
-
{ id: 'pen', label: 'Pen', shortcut: 'B' },
|
|
3497
|
-
{ id: 'rect', label: 'Rect', shortcut: 'R' },
|
|
3498
|
-
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O' },
|
|
3499
|
-
{ id: 'sticky', label: 'Sticky', shortcut: 'N' },
|
|
3500
|
-
{ id: 'arrow', label: 'Arrow', shortcut: 'A' },
|
|
3501
|
-
{ id: 'text', label: 'Text', shortcut: 'T' },
|
|
3502
|
-
{ id: 'eraser', label: 'Eraser', shortcut: 'E' },
|
|
3503
|
-
]}
|
|
3520
|
+
items={items}
|
|
3504
3521
|
/>
|
|
3505
3522
|
);
|
|
3506
3523
|
}
|
|
3507
3524
|
|
|
3508
3525
|
// Plan C follow-up — File + Edit menus, previously inert. Both dispatch to real
|
|
3509
3526
|
// shell flows (File) or the in-canvas undo stack / selection bridges (Edit).
|
|
3510
|
-
function FileDropdown({ onAction, onClose, hasCanvas }) {
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3527
|
+
function FileDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
3528
|
+
// Cloud Phase 25 C2 — a viewer keeps the reads (export, handoff, reload,
|
|
3529
|
+
// close); create / assemble / generate / settings are absent.
|
|
3530
|
+
const items = readOnly
|
|
3531
|
+
? [
|
|
3532
|
+
{ id: 'export', label: 'Export…', shortcut: '⇧⌘E' },
|
|
3533
|
+
{ id: 'handoff', label: 'Handoff to production', shortcut: '⇧⌘H' },
|
|
3534
|
+
{ sep: true },
|
|
3535
|
+
{ id: 'reload', label: 'Reload canvas', shortcut: '⌘R', disabled: !hasCanvas },
|
|
3536
|
+
{ id: 'close', label: 'Close canvas', disabled: !hasCanvas },
|
|
3537
|
+
]
|
|
3538
|
+
: [
|
|
3518
3539
|
// Bare N — the browser reserves ⌘N (New Window) and never delivers it.
|
|
3519
3540
|
{ id: 'new', label: 'New canvas…', shortcut: 'N' },
|
|
3520
3541
|
// DDR-150 P4 Task 12 — one-click "udělej z toho video" from the clips
|
|
@@ -3529,12 +3550,24 @@ function FileDropdown({ onAction, onClose, hasCanvas }) {
|
|
|
3529
3550
|
{ sep: true },
|
|
3530
3551
|
{ id: 'reload', label: 'Reload canvas', shortcut: '⌘R', disabled: !hasCanvas },
|
|
3531
3552
|
{ id: 'close', label: 'Close canvas', disabled: !hasCanvas },
|
|
3532
|
-
]
|
|
3533
|
-
|
|
3534
|
-
);
|
|
3553
|
+
];
|
|
3554
|
+
return <DropdownMenu label="File" left={40} onAction={onAction} onClose={onClose} items={items} />;
|
|
3535
3555
|
}
|
|
3536
3556
|
|
|
3537
|
-
function EditDropdown({ onAction, onClose, hasCanvas }) {
|
|
3557
|
+
function EditDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
3558
|
+
// Cloud Phase 25 C2 — a viewer's Edit menu is selection only; undo/redo,
|
|
3559
|
+
// artboard insert and annotation ops are writes.
|
|
3560
|
+
if (readOnly) {
|
|
3561
|
+
return (
|
|
3562
|
+
<DropdownMenu
|
|
3563
|
+
label="Edit"
|
|
3564
|
+
left={90}
|
|
3565
|
+
onAction={onAction}
|
|
3566
|
+
onClose={onClose}
|
|
3567
|
+
items={[{ id: 'deselect-all', label: 'Deselect all', shortcut: 'Esc' }]}
|
|
3568
|
+
/>
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3538
3571
|
return (
|
|
3539
3572
|
<DropdownMenu
|
|
3540
3573
|
label="Edit"
|
|
@@ -3629,6 +3662,9 @@ function Menubar({
|
|
|
3629
3662
|
onReload,
|
|
3630
3663
|
onCloseCanvas,
|
|
3631
3664
|
onInsertArtboard,
|
|
3665
|
+
// Cloud Phase 25 C2 — viewer role: editing entries are absent from every
|
|
3666
|
+
// menu, and the stamp says VIEW ONLY so the absence is legible.
|
|
3667
|
+
readOnly = false,
|
|
3632
3668
|
}) {
|
|
3633
3669
|
const isSystem = activePath === SYSTEM_TAB;
|
|
3634
3670
|
const stamp = isSystem ? 'SYSTEM' : activePath ? 'CANVAS' : 'IDLE';
|
|
@@ -3642,6 +3678,8 @@ function Menubar({
|
|
|
3642
3678
|
<span style={{ color: 'var(--u-fg-3)' }}>no canvas open</span>
|
|
3643
3679
|
);
|
|
3644
3680
|
|
|
3681
|
+
// Cloud Phase 25 C2 — panels a viewer's session doesn't have (edit chrome).
|
|
3682
|
+
const viewerHiddenPanels = new Set(['assistant', 'inspector', 'layers', 'autoopen']);
|
|
3645
3683
|
const panels = [
|
|
3646
3684
|
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
3647
3685
|
{
|
|
@@ -3748,7 +3786,7 @@ function Menubar({
|
|
|
3748
3786
|
checked: printGuidesVisible,
|
|
3749
3787
|
disabled: !activePath || isSystem,
|
|
3750
3788
|
},
|
|
3751
|
-
];
|
|
3789
|
+
].filter((p) => !readOnly || !viewerHiddenPanels.has(p.id));
|
|
3752
3790
|
|
|
3753
3791
|
const DROPDOWN_MENUS = ['file', 'edit', 'view', 'selection', 'tools', 'help'];
|
|
3754
3792
|
function onMenuClick(key) {
|
|
@@ -3839,6 +3877,7 @@ function Menubar({
|
|
|
3839
3877
|
</nav>
|
|
3840
3878
|
{openMenu === 'file' && (
|
|
3841
3879
|
<FileDropdown
|
|
3880
|
+
readOnly={readOnly}
|
|
3842
3881
|
hasCanvas={!!activePath}
|
|
3843
3882
|
onAction={(id) => {
|
|
3844
3883
|
if (id === 'new') onNewCanvas?.();
|
|
@@ -3855,6 +3894,7 @@ function Menubar({
|
|
|
3855
3894
|
)}
|
|
3856
3895
|
{openMenu === 'edit' && (
|
|
3857
3896
|
<EditDropdown
|
|
3897
|
+
readOnly={readOnly}
|
|
3858
3898
|
hasCanvas={!!activePath && !isSystem}
|
|
3859
3899
|
onAction={(id) => {
|
|
3860
3900
|
if (id === 'undo') postToActiveCanvas({ dgn: 'undo' });
|
|
@@ -3894,6 +3934,7 @@ function Menubar({
|
|
|
3894
3934
|
)}
|
|
3895
3935
|
{openMenu === 'selection' && (
|
|
3896
3936
|
<SelectionDropdown
|
|
3937
|
+
readOnly={readOnly}
|
|
3897
3938
|
onAction={(id) => {
|
|
3898
3939
|
if (id === 'deselect-all') postToActiveCanvas({ dgn: 'selection-clear' });
|
|
3899
3940
|
else if (id === 'select-all-annotations')
|
|
@@ -3904,6 +3945,7 @@ function Menubar({
|
|
|
3904
3945
|
)}
|
|
3905
3946
|
{openMenu === 'tools' && (
|
|
3906
3947
|
<ToolsDropdown
|
|
3948
|
+
readOnly={readOnly}
|
|
3907
3949
|
onAction={(tool) => postToActiveCanvas({ dgn: 'tool-set', tool })}
|
|
3908
3950
|
onClose={() => setOpenMenu(null)}
|
|
3909
3951
|
/>
|
|
@@ -3926,7 +3968,16 @@ function Menubar({
|
|
|
3926
3968
|
)}
|
|
3927
3969
|
<div className="st-mb-right" data-tour="status">
|
|
3928
3970
|
{presence ? <div className="st-presence">{presence}</div> : null}
|
|
3929
|
-
{
|
|
3971
|
+
{readOnly && (
|
|
3972
|
+
<span
|
|
3973
|
+
className="st-stamp st-stamp--viewonly"
|
|
3974
|
+
data-testid="view-only-stamp"
|
|
3975
|
+
data-tip="Your role in this project is viewer — you can browse, comment when available, and export. Ask a project owner for edit access."
|
|
3976
|
+
>
|
|
3977
|
+
VIEW ONLY
|
|
3978
|
+
</span>
|
|
3979
|
+
)}
|
|
3980
|
+
{isNativeApp() && !readOnly && (
|
|
3930
3981
|
<button
|
|
3931
3982
|
type="button"
|
|
3932
3983
|
className="st-assistant"
|
|
@@ -4607,44 +4658,48 @@ function CommentsPanel({
|
|
|
4607
4658
|
<span className="st-comment-sel" title={(c.dom_path || []).join(' > ')}>
|
|
4608
4659
|
{c.selector || '—'}
|
|
4609
4660
|
</span>
|
|
4610
|
-
|
|
4611
|
-
|
|
4661
|
+
{/* Cloud Phase 25 C2 — absent handlers (viewer) ⇒ no
|
|
4662
|
+
mutating actions; the thread stays readable. */}
|
|
4663
|
+
{onResolve && onReopen && onDelete ? (
|
|
4664
|
+
<span className="st-mini-act">
|
|
4665
|
+
{c.status === 'resolved' ? (
|
|
4666
|
+
<button
|
|
4667
|
+
type="button"
|
|
4668
|
+
className="st-iconbtn"
|
|
4669
|
+
aria-label="Reopen"
|
|
4670
|
+
onClick={(e) => {
|
|
4671
|
+
e.stopPropagation();
|
|
4672
|
+
onReopen(c.id);
|
|
4673
|
+
}}
|
|
4674
|
+
>
|
|
4675
|
+
<StIcon name="reopen" size={14} />
|
|
4676
|
+
</button>
|
|
4677
|
+
) : (
|
|
4678
|
+
<button
|
|
4679
|
+
type="button"
|
|
4680
|
+
className="st-iconbtn"
|
|
4681
|
+
aria-label="Resolve"
|
|
4682
|
+
onClick={(e) => {
|
|
4683
|
+
e.stopPropagation();
|
|
4684
|
+
onResolve(c.id);
|
|
4685
|
+
}}
|
|
4686
|
+
>
|
|
4687
|
+
<StIcon name="resolve" size={14} />
|
|
4688
|
+
</button>
|
|
4689
|
+
)}
|
|
4612
4690
|
<button
|
|
4613
4691
|
type="button"
|
|
4614
4692
|
className="st-iconbtn"
|
|
4615
|
-
aria-label="
|
|
4693
|
+
aria-label="Delete"
|
|
4616
4694
|
onClick={(e) => {
|
|
4617
4695
|
e.stopPropagation();
|
|
4618
|
-
|
|
4696
|
+
onDelete(c.id);
|
|
4619
4697
|
}}
|
|
4620
4698
|
>
|
|
4621
|
-
<StIcon name="
|
|
4699
|
+
<StIcon name="x" size={14} />
|
|
4622
4700
|
</button>
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
type="button"
|
|
4626
|
-
className="st-iconbtn"
|
|
4627
|
-
aria-label="Resolve"
|
|
4628
|
-
onClick={(e) => {
|
|
4629
|
-
e.stopPropagation();
|
|
4630
|
-
onResolve(c.id);
|
|
4631
|
-
}}
|
|
4632
|
-
>
|
|
4633
|
-
<StIcon name="resolve" size={14} />
|
|
4634
|
-
</button>
|
|
4635
|
-
)}
|
|
4636
|
-
<button
|
|
4637
|
-
type="button"
|
|
4638
|
-
className="st-iconbtn"
|
|
4639
|
-
aria-label="Delete"
|
|
4640
|
-
onClick={(e) => {
|
|
4641
|
-
e.stopPropagation();
|
|
4642
|
-
onDelete(c.id);
|
|
4643
|
-
}}
|
|
4644
|
-
>
|
|
4645
|
-
<StIcon name="x" size={14} />
|
|
4646
|
-
</button>
|
|
4647
|
-
</span>
|
|
4701
|
+
</span>
|
|
4702
|
+
) : null}
|
|
4648
4703
|
</div>
|
|
4649
4704
|
</div>
|
|
4650
4705
|
))}
|
|
@@ -9110,6 +9165,9 @@ function App() {
|
|
|
9110
9165
|
// it mid-session) — informs canvasUrl() so TSX iframes can pass the right
|
|
9111
9166
|
// ?designRel + ?tokens query to the canvas mount shell.
|
|
9112
9167
|
const [cfg, setCfg] = useState({ designRel: '.design' });
|
|
9168
|
+
// Cloud Phase 25 C2 — viewer role, known at boot from /_config. Every
|
|
9169
|
+
// editing affordance in the shell gates on this (absent, not hidden).
|
|
9170
|
+
const viewerMode = !!cfg.readOnly;
|
|
9113
9171
|
const loadServerConfig = useCallback(() => {
|
|
9114
9172
|
fetch('/_config')
|
|
9115
9173
|
.then((r) => r.json())
|
|
@@ -9132,6 +9190,12 @@ function App() {
|
|
|
9132
9190
|
// from the main origin's /_api). Absent on older servers → relative
|
|
9133
9191
|
// URL fallback keeps same-origin behavior.
|
|
9134
9192
|
canvasOrigin: data.canvasOrigin,
|
|
9193
|
+
// Cloud Phase 25 C2 — the linked hub vouched a `viewer` role at
|
|
9194
|
+
// sign-in. Known at BOOT (before anything draws), so editing
|
|
9195
|
+
// affordances are ABSENT rather than shown-then-refused. This flag
|
|
9196
|
+
// decides what the UI offers; the cell (C1) and the dev-server's
|
|
9197
|
+
// read-only gate (http.ts) are what actually stop a write.
|
|
9198
|
+
readOnly: !!data.readOnly,
|
|
9135
9199
|
}));
|
|
9136
9200
|
})
|
|
9137
9201
|
.catch(() => {});
|
|
@@ -9468,12 +9532,13 @@ function App() {
|
|
|
9468
9532
|
const maybeAutoOpenInspectorOnSelect = useCallback(
|
|
9469
9533
|
(sel) => {
|
|
9470
9534
|
if (!autoOpenInspectorRef.current) return;
|
|
9535
|
+
if (viewerMode) return; // Cloud Phase 25 C2 — inspector is edit chrome
|
|
9471
9536
|
if (!sel || !sel.id) return; // need a stable element id to inspect
|
|
9472
9537
|
if (anyRightPanelOpenRef.current) return; // don't steal from an open panel
|
|
9473
9538
|
openRightPanel('inspector');
|
|
9474
9539
|
setInspectorTab('css');
|
|
9475
9540
|
},
|
|
9476
|
-
[openRightPanel]
|
|
9541
|
+
[openRightPanel, viewerMode]
|
|
9477
9542
|
);
|
|
9478
9543
|
// DDR-148 — the Timeline is a BOTTOM dock, independent of the right rail: it
|
|
9479
9544
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
@@ -11334,7 +11399,7 @@ function App() {
|
|
|
11334
11399
|
document.body.style.cursor = '';
|
|
11335
11400
|
const el = document.getElementById('dc-app-cursor');
|
|
11336
11401
|
if (el) el.textContent = '';
|
|
11337
|
-
browseFirstRunHint();
|
|
11402
|
+
browseFirstRunHint(viewerMode);
|
|
11338
11403
|
return;
|
|
11339
11404
|
}
|
|
11340
11405
|
const cursor = resolveToolCursor(m.tool);
|
|
@@ -13321,15 +13386,17 @@ function App() {
|
|
|
13321
13386
|
}
|
|
13322
13387
|
// Cmd+Shift+I — toggle Inspector. Was bare "I", which collided with the
|
|
13323
13388
|
// canvas highlighter tool (same letter, different action by focus).
|
|
13389
|
+
// Cloud Phase 25 C2 — absent for a viewer (the panel is edit chrome).
|
|
13324
13390
|
if (meta && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
|
13325
13391
|
e.preventDefault();
|
|
13326
|
-
toggleRightPanel('inspector');
|
|
13392
|
+
if (!viewerMode) toggleRightPanel('inspector');
|
|
13327
13393
|
return;
|
|
13328
13394
|
}
|
|
13329
13395
|
// Phase 31 (DDR-123) — Cmd+Shift+A opens the native ACP chat sidepanel.
|
|
13396
|
+
// Cloud Phase 25 C2 — the agent edits; absent for a viewer.
|
|
13330
13397
|
if (meta && e.shiftKey && (e.key === 'a' || e.key === 'A') && isNativeApp()) {
|
|
13331
13398
|
e.preventDefault();
|
|
13332
|
-
toggleRightPanel('assistant');
|
|
13399
|
+
if (!viewerMode) toggleRightPanel('assistant');
|
|
13333
13400
|
return;
|
|
13334
13401
|
}
|
|
13335
13402
|
// Cmd+Shift+E / Cmd+Shift+H — the File-menu chords, previously
|
|
@@ -13346,9 +13413,10 @@ function App() {
|
|
|
13346
13413
|
}
|
|
13347
13414
|
// Cmd+, — open Settings (AI generation keys). The platform convention for
|
|
13348
13415
|
// a settings/preferences surface (feature-ai-media-generation, DDR-16x).
|
|
13416
|
+
// Cloud Phase 25 C2 — settings mutate project config; absent for a viewer.
|
|
13349
13417
|
if (meta && !e.shiftKey && !e.altKey && e.key === ',') {
|
|
13350
13418
|
e.preventDefault();
|
|
13351
|
-
setSettingsOpen(true);
|
|
13419
|
+
if (!viewerMode) setSettingsOpen(true);
|
|
13352
13420
|
return;
|
|
13353
13421
|
}
|
|
13354
13422
|
// Cmd+Shift+T — toggle the Timeline (video-comp scrub) dock.
|
|
@@ -13475,6 +13543,7 @@ function App() {
|
|
|
13475
13543
|
toggleTimeline,
|
|
13476
13544
|
inspectorTab,
|
|
13477
13545
|
performPhotoUndo,
|
|
13546
|
+
viewerMode,
|
|
13478
13547
|
]);
|
|
13479
13548
|
|
|
13480
13549
|
const registerIframe = useCallback((path, el) => {
|
|
@@ -13648,8 +13717,12 @@ function App() {
|
|
|
13648
13717
|
// feature-configurable-panel-docking — resolve, for each slot, the panels
|
|
13649
13718
|
// assigned to it and which one is active (the open one). Layers is only a
|
|
13650
13719
|
// dockable panel in `separate` mode; Assistant is native-only.
|
|
13651
|
-
const panelAvailable = (id) =>
|
|
13652
|
-
|
|
13720
|
+
const panelAvailable = (id) => {
|
|
13721
|
+
// Cloud Phase 25 C2 — edit-chrome panels don't exist for a viewer: no
|
|
13722
|
+
// dock tab, no shortcut, no empty slot.
|
|
13723
|
+
if (viewerMode && (id === 'assistant' || id === 'inspector' || id === 'layers')) return false;
|
|
13724
|
+
return id === 'assistant' ? isNativeApp() : id === 'layers' ? layersMode === 'separate' : true;
|
|
13725
|
+
};
|
|
13653
13726
|
const idsForSide = (side) =>
|
|
13654
13727
|
DOCK_PANELS.filter(
|
|
13655
13728
|
(p) => panelAvailable(p.id) && (panelSide[p.id] || PANEL_SIDES_DEFAULTS[p.id]) === side
|
|
@@ -13666,8 +13739,11 @@ function App() {
|
|
|
13666
13739
|
const rightIds = idsForSide('right');
|
|
13667
13740
|
const leftActive = leftIds.find((id) => panelIsOpen[id]) || null;
|
|
13668
13741
|
const rightActive = rightIds.find((id) => panelIsOpen[id]) || null;
|
|
13669
|
-
|
|
13670
|
-
const
|
|
13742
|
+
// Cloud Phase 25 C2 — the Assistant edits; a viewer's session never mounts it.
|
|
13743
|
+
const leftHostsAssistant =
|
|
13744
|
+
isNativeApp() && !viewerMode && (panelSide.assistant || 'right') === 'left';
|
|
13745
|
+
const rightHostsAssistant =
|
|
13746
|
+
isNativeApp() && !viewerMode && (panelSide.assistant || 'right') === 'right';
|
|
13671
13747
|
const resizingFor = (id) =>
|
|
13672
13748
|
(panelSide[id] || PANEL_SIDES_DEFAULTS[id]) === 'left' ? dragSide === 'sb' : dragSide === 'rp';
|
|
13673
13749
|
const activeCanvasFile =
|
|
@@ -13677,9 +13753,13 @@ function App() {
|
|
|
13677
13753
|
// which owns the resizable width). Assistant is handled separately below as an
|
|
13678
13754
|
// always-mounted ChatPanel so its stream survives a tab switch.
|
|
13679
13755
|
const renderPanelBody = (id) => {
|
|
13756
|
+
// Cloud Phase 25 C2 — the Inspector/Layers panels are edit chrome; a
|
|
13757
|
+
// viewer's session never mounts them (absent, not hidden).
|
|
13758
|
+
if (viewerMode && (id === 'inspector' || id === 'layers')) return null;
|
|
13680
13759
|
if (id === 'tree')
|
|
13681
13760
|
return (
|
|
13682
13761
|
<Sidebar
|
|
13762
|
+
readOnly={viewerMode}
|
|
13683
13763
|
groups={groups}
|
|
13684
13764
|
activePath={activePath}
|
|
13685
13765
|
activeDsName={activePath === SYSTEM_TAB ? (systemData?.ds?.name ?? null) : null}
|
|
@@ -13715,7 +13795,7 @@ function App() {
|
|
|
13715
13795
|
<GitPanel
|
|
13716
13796
|
status={gitStatus && remoteSync ? { ...gitStatus, ...remoteSync } : gitStatus}
|
|
13717
13797
|
project={project}
|
|
13718
|
-
readOnly={!isNativeApp()}
|
|
13798
|
+
readOnly={!isNativeApp() || viewerMode}
|
|
13719
13799
|
resizing={resizingFor('changes')}
|
|
13720
13800
|
onClose={() => setChangesOpen(false)}
|
|
13721
13801
|
onCommit={gitCommit}
|
|
@@ -13795,9 +13875,11 @@ function App() {
|
|
|
13795
13875
|
activePath={activePath}
|
|
13796
13876
|
focusedId={focusedCommentId}
|
|
13797
13877
|
onJump={jumpToComment}
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13878
|
+
// Cloud Phase 25 C2 — a viewer READS threads; the mutating actions
|
|
13879
|
+
// are absent until C3 lands comments on the cell's allowlist.
|
|
13880
|
+
onResolve={viewerMode ? undefined : resolveComment}
|
|
13881
|
+
onReopen={viewerMode ? undefined : reopenComment}
|
|
13882
|
+
onDelete={viewerMode ? undefined : deleteComment}
|
|
13801
13883
|
resizing={resizingFor('comments')}
|
|
13802
13884
|
/>
|
|
13803
13885
|
);
|
|
@@ -13837,6 +13919,7 @@ function App() {
|
|
|
13837
13919
|
)}
|
|
13838
13920
|
<div className="st-shell">
|
|
13839
13921
|
<Menubar
|
|
13922
|
+
readOnly={viewerMode}
|
|
13840
13923
|
activePath={activePath}
|
|
13841
13924
|
project={project}
|
|
13842
13925
|
tabsCount={tabs.length}
|
|
@@ -13994,7 +14077,9 @@ function App() {
|
|
|
13994
14077
|
cfg={cfg}
|
|
13995
14078
|
loadingPath={loadingPath}
|
|
13996
14079
|
onIframeLoad={onIframeLoad}
|
|
13997
|
-
showQuickSetup={
|
|
14080
|
+
showQuickSetup={
|
|
14081
|
+
isNativeApp() && !viewerMode && !!setupReadiness && !setupReadiness.ready
|
|
14082
|
+
}
|
|
13998
14083
|
onStartQuickSetup={() => setQuickSetupOpen(true)}
|
|
13999
14084
|
/>
|
|
14000
14085
|
</div>
|
|
@@ -35,6 +35,12 @@ export function canvasUrl(p, cfg, opts) {
|
|
|
35
35
|
// Phase 27 (E2) — DiffView renders a CLEAN canvas (no editor chrome: toolbar,
|
|
36
36
|
// world minimap, devtools) so the before/after is just the design.
|
|
37
37
|
if (opts?.hideChrome) params.set('hide-chrome', '1');
|
|
38
|
+
// Cloud Phase 25 C2 — the linked hub vouched a viewer role at sign-in
|
|
39
|
+
// (`/_config.readOnly`), so the canvas boots read-only: edit tools and
|
|
40
|
+
// mutating interactions are ABSENT from the iframe chrome (read-only-mode.ts
|
|
41
|
+
// reads this flag). Boot-static on purpose — a role is per-session, and a
|
|
42
|
+
// query param can't race the way a post-load message could.
|
|
43
|
+
if (cfg?.readOnly) params.set('ro', '1');
|
|
38
44
|
const ds0 = cfg?.designSystems?.[0];
|
|
39
45
|
// Specimen detection: anything under `system/<ds>/preview/` belongs to that
|
|
40
46
|
// specific DS, so it must render with *that* DS's tokens — not always the
|
|
@@ -69,6 +69,13 @@
|
|
|
69
69
|
padding: var(--space-1) var(--space-3); border: 1px solid var(--accent-muted);
|
|
70
70
|
border-radius: var(--radius-pill); background: var(--accent-tint);
|
|
71
71
|
}
|
|
72
|
+
/* Cloud Phase 25 C2 — the viewer-role stamp. Warning hue so "why is editing
|
|
73
|
+
missing" has a visible, hoverable answer in the chrome. */
|
|
74
|
+
.st-stamp--viewonly {
|
|
75
|
+
color: var(--status-warning, var(--accent));
|
|
76
|
+
border-color: color-mix(in oklab, var(--status-warning, var(--accent)) 40%, transparent);
|
|
77
|
+
background: color-mix(in oklab, var(--status-warning, var(--accent)) 12%, transparent);
|
|
78
|
+
}
|
|
72
79
|
.st-mb-file { font-family: var(--font-mono); font-size: var(--type-xs); color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 260px; }
|
|
73
80
|
.st-mb-file b { color: var(--fg-0); font-weight: 600; }
|
|
74
81
|
.st-mb-sep { width: 1px; height: 16px; background: var(--border-default); }
|