@1agh/maude 0.60.6 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/acp/index.ts +1 -0
- package/apps/studio/ai-banner.tsx +1 -0
- package/apps/studio/annotations-context-toolbar.tsx +3 -1
- package/apps/studio/annotations-layer.tsx +33 -16
- package/apps/studio/api.ts +108 -18
- package/apps/studio/artboard-guides-overlay.tsx +5 -1
- package/apps/studio/assets-s3.ts +6 -1
- package/apps/studio/bin/_import-figma.mjs +8 -3
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-artifacts.ts +21 -0
- package/apps/studio/canvas-build.ts +14 -9
- package/apps/studio/canvas-comment-mount.tsx +27 -30
- package/apps/studio/canvas-icons.tsx +1 -1
- package/apps/studio/canvas-lib.tsx +94 -5
- package/apps/studio/canvas-list-watch.ts +14 -1
- package/apps/studio/canvas-shell.tsx +8 -0
- package/apps/studio/client/app.jsx +224 -45
- package/apps/studio/client/panels/GitPanel.jsx +121 -13
- package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
- package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
- package/apps/studio/client/panels/SyncPanel.jsx +595 -1
- package/apps/studio/client/styles/3-shell-maude.css +38 -0
- package/apps/studio/clip-ops.ts +8 -1
- package/apps/studio/cloud/endpoints.ts +265 -3
- package/apps/studio/collab/origins.ts +3 -1
- package/apps/studio/comments-overlay.tsx +5 -0
- package/apps/studio/config.schema.json +24 -0
- package/apps/studio/context-menu.tsx +40 -27
- package/apps/studio/context.ts +57 -8
- package/apps/studio/cursors-overlay.tsx +25 -13
- package/apps/studio/dist/client.bundle.js +686 -686
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/jobs.ts +77 -15
- package/apps/studio/exporters/remote.ts +190 -0
- package/apps/studio/exporters/video-encode-lib.ts +10 -4
- package/apps/studio/figma/to-strokes.ts +11 -9
- package/apps/studio/gifenc.d.ts +51 -0
- package/apps/studio/git/log-format.ts +88 -0
- package/apps/studio/git/safe-rel.ts +96 -0
- package/apps/studio/git/service.ts +46 -27
- package/apps/studio/hmr-broadcast.ts +10 -0
- package/apps/studio/http.ts +384 -6
- package/apps/studio/participants-chrome.tsx +1 -0
- package/apps/studio/photo-store.ts +7 -0
- package/apps/studio/react-augment.d.ts +17 -0
- package/apps/studio/runtime-bundle.ts +6 -1
- package/apps/studio/server.ts +43 -8
- package/apps/studio/sync/agent.ts +70 -82
- package/apps/studio/sync/asset-push.ts +28 -6
- package/apps/studio/sync/autocommit.ts +106 -5
- package/apps/studio/sync/cell-file-events.ts +117 -0
- package/apps/studio/sync/cell-pairing.ts +20 -5
- package/apps/studio/sync/cell-write-nudge.ts +244 -0
- package/apps/studio/sync/codec.ts +155 -3
- package/apps/studio/sync/cold-start-apply.ts +211 -0
- package/apps/studio/sync/ctl-heal.ts +253 -0
- package/apps/studio/sync/ctl-provider.ts +217 -0
- package/apps/studio/sync/decide-file.ts +335 -0
- package/apps/studio/sync/file-ledger.ts +581 -0
- package/apps/studio/sync/file-membership.ts +32 -0
- package/apps/studio/sync/file-plane.ts +1400 -0
- package/apps/studio/sync/file-pull.ts +41 -4
- package/apps/studio/sync/hub-link.ts +16 -1
- package/apps/studio/sync/hub-listing.ts +46 -0
- package/apps/studio/sync/hubs-config.ts +16 -0
- package/apps/studio/sync/index.ts +919 -214
- package/apps/studio/sync/journal-client.ts +200 -0
- package/apps/studio/sync/migrate-seed.ts +99 -67
- package/apps/studio/sync/poke.ts +50 -0
- package/apps/studio/sync/projection.ts +13 -0
- package/apps/studio/sync/pull-budget.ts +86 -0
- package/apps/studio/sync/settings.ts +110 -0
- package/apps/studio/sync/status.ts +68 -0
- package/apps/studio/sync/trash.ts +243 -0
- package/apps/studio/sync/untrusted.ts +30 -10
- package/apps/studio/test/_helpers.ts +8 -0
- package/apps/studio/test/canvas-build.test.ts +63 -0
- package/apps/studio/test/canvas-list-watch.test.ts +17 -0
- package/apps/studio/test/canvas-move-api.test.ts +31 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
- package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
- package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
- package/apps/studio/test/cloud-history-posture.test.ts +230 -0
- package/apps/studio/test/cloud-session-role.test.ts +30 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
- package/apps/studio/test/cold-start-apply.test.ts +303 -0
- package/apps/studio/test/collab-stress.test.ts +9 -1
- package/apps/studio/test/export-lane.test.ts +245 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
- package/apps/studio/test/git-log-format.test.ts +95 -0
- package/apps/studio/test/git-safe-rel.test.ts +132 -0
- package/apps/studio/test/hmr-broadcast.test.ts +26 -0
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
- package/apps/studio/test/sync-agent.test.ts +78 -0
- package/apps/studio/test/sync-asset-push.test.ts +90 -16
- package/apps/studio/test/sync-autocommit.test.ts +80 -0
- package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
- package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
- package/apps/studio/test/sync-decide-file.test.ts +420 -0
- package/apps/studio/test/sync-file-ledger.test.ts +334 -0
- package/apps/studio/test/sync-file-membership.test.ts +17 -1
- package/apps/studio/test/sync-file-plane.test.ts +976 -0
- package/apps/studio/test/sync-hub-listing.test.ts +46 -0
- package/apps/studio/test/sync-meta-codec.test.ts +76 -0
- package/apps/studio/test/sync-move-retirement.test.ts +231 -0
- package/apps/studio/test/sync-panel-surface.test.ts +20 -0
- package/apps/studio/test/sync-path-pull.test.ts +67 -1
- package/apps/studio/test/sync-pull-budget.test.ts +169 -0
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
- package/apps/studio/test/sync-settings-routes.test.ts +195 -0
- package/apps/studio/test/sync-settings.test.ts +151 -0
- package/apps/studio/test/sync-status.test.ts +69 -0
- package/apps/studio/test/sync-trash.test.ts +132 -0
- package/apps/studio/test/workspace-containment.test.ts +45 -9
- package/apps/studio/tsconfig.json +9 -10
- package/apps/studio/use-annotation-resize.tsx +14 -3
- package/apps/studio/use-collab.tsx +3 -1
- package/apps/studio/whats-new.json +99 -0
- package/apps/studio/workspace-mode.ts +110 -62
- package/apps/studio/ws.ts +22 -1
- package/cli/bin/claude-design-server.mjs +19 -0
- package/cli/commands/design.mjs +25 -6
- package/cli/commands/hub-workspace.mjs +243 -22
- package/cli/commands/hub-workspace.test.mjs +171 -0
- package/cli/commands/hub.mjs +71 -1
- package/cli/lib/design-link.mjs +186 -1
- package/cli/lib/design-ownership.mjs +330 -0
- package/cli/lib/design-ownership.test.mjs +329 -0
- package/cli/lib/hubs-config.mjs +21 -0
- package/cli/lib/hubs-config.test.mjs +47 -1
- package/cli/lib/workspace-plan.mjs +298 -5
- package/cli/lib/workspace-plan.test.mjs +215 -1
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +43 -2
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/apps/studio/server.mjs +0 -1312
- package/apps/studio/sync/asset-pull.ts +0 -210
- package/apps/studio/sync/asset-push-worker.ts +0 -84
- package/apps/studio/sync/asset-sweep.ts +0 -262
- package/apps/studio/test/sync-asset-pull.test.ts +0 -161
- package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
- package/apps/studio/test/sync-asset-sweep.test.ts +0 -243
|
@@ -174,8 +174,20 @@ export default function GitPanel({
|
|
|
174
174
|
// PRESENTATION, NOT A CONTROL — `.git` is untouched, a terminal `git` works
|
|
175
175
|
// exactly as before, and Disconnect (CloudBar) restores the panel live.
|
|
176
176
|
cloudManaged = false,
|
|
177
|
+
// WHOSE HISTORY THESE ROWS ARE (feature-cloud-managed-git-posture).
|
|
178
|
+
//
|
|
179
|
+
// Decided by App, at the one place the posture is named, and handed down —
|
|
180
|
+
// the panel must NOT re-derive it from `cloudManaged`, because the whole
|
|
181
|
+
// point of the constant is that this rule has exactly one spelling. `loadLog`
|
|
182
|
+
// is swapped at the same call site; this prop is only how the panel knows
|
|
183
|
+
// which repo the rows it just received describe, so it can name it.
|
|
184
|
+
historySource = 'local',
|
|
185
|
+
// `{ branch, project, hubHost }` — what the cell reported alongside the log.
|
|
186
|
+
// Null until the first successful cloud load.
|
|
187
|
+
cloudHistory = null,
|
|
177
188
|
}) {
|
|
178
189
|
const withdrawn = historyOnly || cloudManaged;
|
|
190
|
+
const cloudHistorySource = historySource === 'cloud';
|
|
179
191
|
const [tab, setTab] = useState(withdrawn ? 'history' : 'changes');
|
|
180
192
|
// `cloudManaged` flips at runtime (connect/disconnect) — the initial tab
|
|
181
193
|
// state alone would leave a freshly-linked panel sitting on Changes.
|
|
@@ -193,6 +205,13 @@ export default function GitPanel({
|
|
|
193
205
|
// versions are click-to-preview), '' → repo-wide read-only list. `undefined`
|
|
194
206
|
// = nothing loaded yet. (phase-27.1)
|
|
195
207
|
const [logScope, setLogScope] = useState(undefined);
|
|
208
|
+
// A FAILED LOAD IS NOT AN EMPTY HISTORY. The local reader could only ever
|
|
209
|
+
// succeed (it reads a repo on this disk), so `log === []` meant one thing.
|
|
210
|
+
// The cloud reader can fail, and reporting that as "No saved versions yet"
|
|
211
|
+
// would reproduce — over the network this time — the exact false reassurance
|
|
212
|
+
// this feature exists to delete. `loadLog` answers `null` for "could not
|
|
213
|
+
// reach", `[]` for "nothing saved yet".
|
|
214
|
+
const [logFailed, setLogFailed] = useState(false);
|
|
196
215
|
const selectAllRef = useRef(null);
|
|
197
216
|
|
|
198
217
|
// History is scoped to the open canvas/specimen so each row previews THAT
|
|
@@ -417,9 +436,10 @@ export default function GitPanel({
|
|
|
417
436
|
let cancelled = false;
|
|
418
437
|
setLogLoading(true);
|
|
419
438
|
(async () => {
|
|
420
|
-
const entries =
|
|
439
|
+
const entries = await loadLog(historyScope || undefined);
|
|
421
440
|
if (cancelled) return;
|
|
422
|
-
|
|
441
|
+
setLogFailed(entries == null);
|
|
442
|
+
setLog(entries ?? []);
|
|
423
443
|
setLogScope(historyScope);
|
|
424
444
|
setLogLoading(false);
|
|
425
445
|
})();
|
|
@@ -428,12 +448,38 @@ export default function GitPanel({
|
|
|
428
448
|
};
|
|
429
449
|
}, [tab, historyScope, logScope, loadLog]);
|
|
430
450
|
|
|
451
|
+
// Retry, and re-read on a posture flip. Dropping the cached scope is the
|
|
452
|
+
// panel's existing "reload History" gesture (`run()` already uses it), so
|
|
453
|
+
// Retry needs no second mechanism — and Connect/Disconnect swaps `loadLog`
|
|
454
|
+
// under us, which must re-ask rather than keep showing the other repo's rows.
|
|
455
|
+
const reloadHistory = () => {
|
|
456
|
+
setLogFailed(false);
|
|
457
|
+
setLogScope(undefined);
|
|
458
|
+
};
|
|
459
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
setLogFailed(false);
|
|
462
|
+
setLogScope(undefined);
|
|
463
|
+
}, [historySource]);
|
|
464
|
+
|
|
431
465
|
function toggleAll() {
|
|
432
466
|
setUnchecked((prev) => (prev.size === 0 ? new Set(files.map((f) => f.path)) : new Set()));
|
|
433
467
|
}
|
|
434
468
|
|
|
435
469
|
const count = files.length;
|
|
436
|
-
|
|
470
|
+
// WHICH REPO THE HEADER NAMES. `status` is LOCAL git, and in cloud-managed
|
|
471
|
+
// posture the app stops fetching it entirely — so a header built from it
|
|
472
|
+
// would name the desktop folder and the desktop's branch while the rows
|
|
473
|
+
// underneath list the cell's commits. That mismatch (`desktop-side / main`
|
|
474
|
+
// over the cloud's versions) is half of the reported confusion.
|
|
475
|
+
//
|
|
476
|
+
// The cell reports its own project and branch with the log. The hub host is
|
|
477
|
+
// the fallback: it names SOMETHING true about where these versions live,
|
|
478
|
+
// which the local folder name does not.
|
|
479
|
+
const headerProject = cloudHistorySource
|
|
480
|
+
? (cloudHistory?.project ?? cloudHistory?.hubHost ?? null)
|
|
481
|
+
: project;
|
|
482
|
+
const branch = cloudHistorySource ? (cloudHistory?.branch ?? '') : status?.branch || 'main';
|
|
437
483
|
const unpushed = status?.unpushed || 0;
|
|
438
484
|
const canSave = message.trim().length > 0 && checkedPaths.length > 0 && !busy;
|
|
439
485
|
const notRepo = status && status.repo === false;
|
|
@@ -476,12 +522,20 @@ export default function GitPanel({
|
|
|
476
522
|
<span className="gp-panel-title">{withdrawn ? 'History' : 'Changes'}</span>
|
|
477
523
|
{!withdrawn && count > 0 && <span className="gp-count">{count} unsaved</span>}
|
|
478
524
|
<span className="gp-spacer" />
|
|
479
|
-
<span
|
|
525
|
+
<span
|
|
526
|
+
className="gp-draft"
|
|
527
|
+
data-testid="git-panel-repo"
|
|
528
|
+
title={
|
|
529
|
+
cloudHistorySource
|
|
530
|
+
? 'The cloud project these versions belong to'
|
|
531
|
+
: 'Your project and shared draft'
|
|
532
|
+
}
|
|
533
|
+
>
|
|
480
534
|
<Icon name="folder" size={12} />
|
|
481
|
-
{
|
|
535
|
+
{headerProject ? (
|
|
482
536
|
<>
|
|
483
|
-
<b>{
|
|
484
|
-
<span className="gp-sep">/</span>
|
|
537
|
+
<b>{headerProject}</b>
|
|
538
|
+
{branch ? <span className="gp-sep">/</span> : null}
|
|
485
539
|
</>
|
|
486
540
|
) : null}
|
|
487
541
|
{branch}
|
|
@@ -756,17 +810,71 @@ export default function GitPanel({
|
|
|
756
810
|
<div className="gp-empty">
|
|
757
811
|
<p>Loading history…</p>
|
|
758
812
|
</div>
|
|
813
|
+
) : logFailed ? (
|
|
814
|
+
// A READ FAILURE, SAID AS ONE. Reuses the `callout--warning`
|
|
815
|
+
// vocabulary the banner above already uses, so this is copy and a
|
|
816
|
+
// Retry action rather than a new component.
|
|
817
|
+
<div className="gp-pad">
|
|
818
|
+
<div
|
|
819
|
+
className="callout callout--warning"
|
|
820
|
+
role="status"
|
|
821
|
+
aria-live="polite"
|
|
822
|
+
data-testid="git-history-unreachable"
|
|
823
|
+
>
|
|
824
|
+
<div className="gp-callout-col">
|
|
825
|
+
<span>
|
|
826
|
+
<strong
|
|
827
|
+
style={{
|
|
828
|
+
display: 'block',
|
|
829
|
+
marginBottom: 'var(--space-1)',
|
|
830
|
+
color: 'var(--fg-0)',
|
|
831
|
+
}}
|
|
832
|
+
>
|
|
833
|
+
Couldn’t reach the cloud history.
|
|
834
|
+
</strong>
|
|
835
|
+
Your work is still being saved — this is only the list of versions.
|
|
836
|
+
</span>
|
|
837
|
+
<div className="gp-callout-actions">
|
|
838
|
+
<button
|
|
839
|
+
type="button"
|
|
840
|
+
className="btn btn--sm"
|
|
841
|
+
data-testid="git-history-retry"
|
|
842
|
+
onClick={reloadHistory}
|
|
843
|
+
>
|
|
844
|
+
Retry
|
|
845
|
+
</button>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
759
850
|
) : !log || log.length === 0 ? (
|
|
760
851
|
<div className="gp-empty">
|
|
761
852
|
<span className="gp-empty-glyph">
|
|
762
853
|
<Icon name="history" size={24} />
|
|
763
854
|
</span>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
855
|
+
{cloudHistorySource ? (
|
|
856
|
+
// NEVER OFFER THE SAVE THIS POSTURE HAS WITHDRAWN. The old copy
|
|
857
|
+
// ("Save a version and it'll show up here") sat directly under a
|
|
858
|
+
// "Cloud is saving" note and told the reader to do a thing the
|
|
859
|
+
// panel had just removed the button for — which is how "No saved
|
|
860
|
+
// versions yet" came to read as "nothing is being saved".
|
|
861
|
+
<>
|
|
862
|
+
<h3>No versions yet</h3>
|
|
863
|
+
<p>
|
|
864
|
+
The cloud is saving this project
|
|
865
|
+
{previewable ? `. Versions of ${activeName} appear here as you work.` : '. Versions appear here as you work.'}
|
|
866
|
+
</p>
|
|
867
|
+
</>
|
|
868
|
+
) : (
|
|
869
|
+
<>
|
|
870
|
+
<h3>No saved versions yet</h3>
|
|
871
|
+
<p>
|
|
872
|
+
{previewable
|
|
873
|
+
? `Save a version of ${activeName} and it'll show up here.`
|
|
874
|
+
: "Save a version and it'll show up here."}
|
|
875
|
+
</p>
|
|
876
|
+
</>
|
|
877
|
+
)}
|
|
770
878
|
</div>
|
|
771
879
|
) : (
|
|
772
880
|
log.map((c) => {
|
|
@@ -1182,7 +1182,7 @@ function AppearanceTab({ theme, onSetTheme, cpMode, onSetCpMode }) {
|
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
1184
|
export default function SettingsPanel({
|
|
1185
|
-
/** `{ dashboardUrl
|
|
1185
|
+
/** `{ dashboardUrl?, projectName }` when this is a cloud tab, else null. */
|
|
1186
1186
|
// Tri-state (see the note where this value is created): `undefined` until
|
|
1187
1187
|
// the server config answers, `null` for the desktop, an object for a cloud
|
|
1188
1188
|
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// feature-before-first-external-users Task 2 — the first-upgrade consent
|
|
2
|
+
// dialog. `sharedDoc` / `syncFiles` / `propagateDeletes` all default ON, so a
|
|
3
|
+
// person who links (or upgrades into) a hub gets file mirroring and delete
|
|
4
|
+
// propagation without ever having said yes to either — the round-4 ADVOCATE
|
|
5
|
+
// finding. This asks ONCE per (hub, machine), the first time a sync payload
|
|
6
|
+
// for that hub arrives, and records the answer.
|
|
7
|
+
//
|
|
8
|
+
// Deliberately NOT inside the Sync panel: a consent that only appears if you
|
|
9
|
+
// happen to open a panel is not consent. It mounts with the global banners in
|
|
10
|
+
// app.jsx and renders over everything (st-dialog anatomy).
|
|
11
|
+
//
|
|
12
|
+
// The answer is machine-local (localStorage, like the notice acks) — consent
|
|
13
|
+
// is per human per machine, not a project setting to sync around. "Keep
|
|
14
|
+
// syncing" records acceptance and changes nothing; "Limit sync" turns
|
|
15
|
+
// `syncFiles` + `propagateDeletes` off through the same `/_api/sync/settings`
|
|
16
|
+
// route the panel's toggles use, so the limited state is inspectable and
|
|
17
|
+
// reversible in Settings, not a hidden third mode.
|
|
18
|
+
|
|
19
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
const CONSENT_KEY = 'maude-sync-consent';
|
|
22
|
+
|
|
23
|
+
function readConsent(url) {
|
|
24
|
+
try {
|
|
25
|
+
const all = JSON.parse(localStorage.getItem(CONSENT_KEY) || '{}');
|
|
26
|
+
const c = all[url];
|
|
27
|
+
return c && typeof c === 'object' ? c : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeConsent(url, choice) {
|
|
34
|
+
try {
|
|
35
|
+
const all = JSON.parse(localStorage.getItem(CONSENT_KEY) || '{}');
|
|
36
|
+
all[url] = { choice, at: Date.now() };
|
|
37
|
+
localStorage.setItem(CONSENT_KEY, JSON.stringify(all));
|
|
38
|
+
} catch {
|
|
39
|
+
/* private mode — the dialog will simply ask again next boot */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Loopback hubs (local dev, a cell's own hub) are consent-by-configuration —
|
|
44
|
+
* nothing leaves the machine, and nagging every dev boot trains people to
|
|
45
|
+
* click through the one dialog that matters. Mirrors isLoopbackHubUrl. */
|
|
46
|
+
function isLoopback(url) {
|
|
47
|
+
try {
|
|
48
|
+
const h = new URL(url).hostname;
|
|
49
|
+
return h === 'localhost' || h === '::1' || /^127\./.test(h) || h === '[::1]';
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function SyncConsentDialog({ status, cloud }) {
|
|
56
|
+
const url = typeof status?.url === 'string' ? status.url : null;
|
|
57
|
+
// `answered` only forces the re-render after a click — the record itself
|
|
58
|
+
// lives in localStorage so a reload cannot re-ask.
|
|
59
|
+
const [answered, setAnswered] = useState(false);
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const [note, setNote] = useState('');
|
|
62
|
+
|
|
63
|
+
// Never on a cell (a member browsing someone's hosted project is not the
|
|
64
|
+
// machine whose files sync), never for loopback, never before sync speaks.
|
|
65
|
+
const due = !cloud && url && !isLoopback(url) && !answered && !readConsent(url);
|
|
66
|
+
|
|
67
|
+
// The dialog steals the screen, so say so to assistive tech the same way
|
|
68
|
+
// the other modals do — but only while actually shown.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!due) return undefined;
|
|
71
|
+
const onKey = (e) => {
|
|
72
|
+
// No Escape-to-dismiss: an unanswered consent must not be dismissable
|
|
73
|
+
// into the accepted-by-silence state. Tab stays inside via the two
|
|
74
|
+
// buttons being the only tabbables in the sheet.
|
|
75
|
+
if (e.key === 'Escape') e.preventDefault();
|
|
76
|
+
};
|
|
77
|
+
window.addEventListener('keydown', onKey, true);
|
|
78
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
79
|
+
}, [due]);
|
|
80
|
+
|
|
81
|
+
const accept = useCallback(() => {
|
|
82
|
+
if (url) writeConsent(url, 'accepted');
|
|
83
|
+
setAnswered(true);
|
|
84
|
+
}, [url]);
|
|
85
|
+
|
|
86
|
+
const limit = useCallback(async () => {
|
|
87
|
+
setBusy(true);
|
|
88
|
+
setNote('');
|
|
89
|
+
let ok = false;
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/_api/sync/settings', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ syncFiles: false, propagateDeletes: false }),
|
|
95
|
+
});
|
|
96
|
+
const json = await res.json().catch(() => null);
|
|
97
|
+
ok = json?.ok === true;
|
|
98
|
+
if (!ok) setNote(json?.detail || 'The limited mode could not be saved — try again.');
|
|
99
|
+
} catch {
|
|
100
|
+
setNote('Maude could not reach the sync service — try again.');
|
|
101
|
+
}
|
|
102
|
+
setBusy(false);
|
|
103
|
+
if (ok) {
|
|
104
|
+
if (url) writeConsent(url, 'limited');
|
|
105
|
+
setAnswered(true);
|
|
106
|
+
}
|
|
107
|
+
}, [url]);
|
|
108
|
+
|
|
109
|
+
if (!due) return null;
|
|
110
|
+
|
|
111
|
+
let host = url;
|
|
112
|
+
try {
|
|
113
|
+
host = new URL(url).host;
|
|
114
|
+
} catch {
|
|
115
|
+
/* keep the raw string */
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="st-scrim" data-testid="sync-consent">
|
|
120
|
+
<div
|
|
121
|
+
className="st-dialog st-consent"
|
|
122
|
+
role="dialog"
|
|
123
|
+
aria-modal="true"
|
|
124
|
+
aria-labelledby="sync-consent-title"
|
|
125
|
+
>
|
|
126
|
+
<div className="st-dialog-hd">
|
|
127
|
+
<span className="st-dialog-title" id="sync-consent-title">
|
|
128
|
+
This project syncs with {host}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="st-dialog-bd">
|
|
132
|
+
<p>Since your last update, syncing covers more than canvases. With this workspace:</p>
|
|
133
|
+
<ul className="st-consent-list">
|
|
134
|
+
<li>
|
|
135
|
+
<strong>The whole design folder mirrors both ways</strong> — stylesheets, docs and
|
|
136
|
+
code modules, not only canvases.
|
|
137
|
+
</li>
|
|
138
|
+
<li>
|
|
139
|
+
<strong>Deleting a file here deletes it there</strong> (and the other way round).
|
|
140
|
+
Replaced copies are kept in the Sync panel's Trash, and bulk removals pause for confirmation.
|
|
141
|
+
</li>
|
|
142
|
+
{status?.sharedDoc && (
|
|
143
|
+
<li>
|
|
144
|
+
<strong>Your live editing buffer is the shared object</strong> — edits stream to
|
|
145
|
+
the workspace as you type, not as saved copies.
|
|
146
|
+
</li>
|
|
147
|
+
)}
|
|
148
|
+
</ul>
|
|
149
|
+
<p className="st-consent-fine">
|
|
150
|
+
You can change either choice any time in the Sync panel's Settings. Only sync with
|
|
151
|
+
workspaces you operate or trust.
|
|
152
|
+
</p>
|
|
153
|
+
{note && (
|
|
154
|
+
<p className="st-consent-note" role="status" aria-live="polite">
|
|
155
|
+
{note}
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
<div className="st-dialog-ft">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="btn btn--ghost"
|
|
163
|
+
data-testid="sync-consent-limit"
|
|
164
|
+
disabled={busy}
|
|
165
|
+
onClick={limit}
|
|
166
|
+
>
|
|
167
|
+
{busy ? 'Saving…' : 'Sync canvases only'}
|
|
168
|
+
</button>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="btn btn--primary"
|
|
172
|
+
data-testid="sync-consent-accept"
|
|
173
|
+
disabled={busy}
|
|
174
|
+
onClick={accept}
|
|
175
|
+
>
|
|
176
|
+
Keep syncing everything
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|