@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.
Files changed (145) hide show
  1. package/apps/studio/acp/index.ts +1 -0
  2. package/apps/studio/ai-banner.tsx +1 -0
  3. package/apps/studio/annotations-context-toolbar.tsx +3 -1
  4. package/apps/studio/annotations-layer.tsx +33 -16
  5. package/apps/studio/api.ts +108 -18
  6. package/apps/studio/artboard-guides-overlay.tsx +5 -1
  7. package/apps/studio/assets-s3.ts +6 -1
  8. package/apps/studio/bin/_import-figma.mjs +8 -3
  9. package/apps/studio/build.ts +1 -1
  10. package/apps/studio/canvas-artifacts.ts +21 -0
  11. package/apps/studio/canvas-build.ts +14 -9
  12. package/apps/studio/canvas-comment-mount.tsx +27 -30
  13. package/apps/studio/canvas-icons.tsx +1 -1
  14. package/apps/studio/canvas-lib.tsx +94 -5
  15. package/apps/studio/canvas-list-watch.ts +14 -1
  16. package/apps/studio/canvas-shell.tsx +8 -0
  17. package/apps/studio/client/app.jsx +224 -45
  18. package/apps/studio/client/panels/GitPanel.jsx +121 -13
  19. package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
  20. package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
  21. package/apps/studio/client/panels/SyncPanel.jsx +595 -1
  22. package/apps/studio/client/styles/3-shell-maude.css +38 -0
  23. package/apps/studio/clip-ops.ts +8 -1
  24. package/apps/studio/cloud/endpoints.ts +265 -3
  25. package/apps/studio/collab/origins.ts +3 -1
  26. package/apps/studio/comments-overlay.tsx +5 -0
  27. package/apps/studio/config.schema.json +24 -0
  28. package/apps/studio/context-menu.tsx +40 -27
  29. package/apps/studio/context.ts +57 -8
  30. package/apps/studio/cursors-overlay.tsx +25 -13
  31. package/apps/studio/dist/client.bundle.js +686 -686
  32. package/apps/studio/dist/comment-mount.js +2 -2
  33. package/apps/studio/dist/styles.css +1 -1
  34. package/apps/studio/exporters/jobs.ts +77 -15
  35. package/apps/studio/exporters/remote.ts +190 -0
  36. package/apps/studio/exporters/video-encode-lib.ts +10 -4
  37. package/apps/studio/figma/to-strokes.ts +11 -9
  38. package/apps/studio/gifenc.d.ts +51 -0
  39. package/apps/studio/git/log-format.ts +88 -0
  40. package/apps/studio/git/safe-rel.ts +96 -0
  41. package/apps/studio/git/service.ts +46 -27
  42. package/apps/studio/hmr-broadcast.ts +10 -0
  43. package/apps/studio/http.ts +384 -6
  44. package/apps/studio/participants-chrome.tsx +1 -0
  45. package/apps/studio/photo-store.ts +7 -0
  46. package/apps/studio/react-augment.d.ts +17 -0
  47. package/apps/studio/runtime-bundle.ts +6 -1
  48. package/apps/studio/server.ts +43 -8
  49. package/apps/studio/sync/agent.ts +70 -82
  50. package/apps/studio/sync/asset-push.ts +28 -6
  51. package/apps/studio/sync/autocommit.ts +106 -5
  52. package/apps/studio/sync/cell-file-events.ts +117 -0
  53. package/apps/studio/sync/cell-pairing.ts +20 -5
  54. package/apps/studio/sync/cell-write-nudge.ts +244 -0
  55. package/apps/studio/sync/codec.ts +155 -3
  56. package/apps/studio/sync/cold-start-apply.ts +211 -0
  57. package/apps/studio/sync/ctl-heal.ts +253 -0
  58. package/apps/studio/sync/ctl-provider.ts +217 -0
  59. package/apps/studio/sync/decide-file.ts +335 -0
  60. package/apps/studio/sync/file-ledger.ts +581 -0
  61. package/apps/studio/sync/file-membership.ts +32 -0
  62. package/apps/studio/sync/file-plane.ts +1400 -0
  63. package/apps/studio/sync/file-pull.ts +41 -4
  64. package/apps/studio/sync/hub-link.ts +16 -1
  65. package/apps/studio/sync/hub-listing.ts +46 -0
  66. package/apps/studio/sync/hubs-config.ts +16 -0
  67. package/apps/studio/sync/index.ts +919 -214
  68. package/apps/studio/sync/journal-client.ts +200 -0
  69. package/apps/studio/sync/migrate-seed.ts +99 -67
  70. package/apps/studio/sync/poke.ts +50 -0
  71. package/apps/studio/sync/projection.ts +13 -0
  72. package/apps/studio/sync/pull-budget.ts +86 -0
  73. package/apps/studio/sync/settings.ts +110 -0
  74. package/apps/studio/sync/status.ts +68 -0
  75. package/apps/studio/sync/trash.ts +243 -0
  76. package/apps/studio/sync/untrusted.ts +30 -10
  77. package/apps/studio/test/_helpers.ts +8 -0
  78. package/apps/studio/test/canvas-build.test.ts +63 -0
  79. package/apps/studio/test/canvas-list-watch.test.ts +17 -0
  80. package/apps/studio/test/canvas-move-api.test.ts +31 -0
  81. package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
  82. package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
  83. package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
  84. package/apps/studio/test/cloud-history-posture.test.ts +230 -0
  85. package/apps/studio/test/cloud-session-role.test.ts +30 -0
  86. package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
  87. package/apps/studio/test/cold-start-apply.test.ts +303 -0
  88. package/apps/studio/test/collab-stress.test.ts +9 -1
  89. package/apps/studio/test/export-lane.test.ts +245 -0
  90. package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
  91. package/apps/studio/test/git-log-format.test.ts +95 -0
  92. package/apps/studio/test/git-safe-rel.test.ts +132 -0
  93. package/apps/studio/test/hmr-broadcast.test.ts +26 -0
  94. package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
  95. package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
  96. package/apps/studio/test/sync-agent.test.ts +78 -0
  97. package/apps/studio/test/sync-asset-push.test.ts +90 -16
  98. package/apps/studio/test/sync-autocommit.test.ts +80 -0
  99. package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
  100. package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
  101. package/apps/studio/test/sync-decide-file.test.ts +420 -0
  102. package/apps/studio/test/sync-file-ledger.test.ts +334 -0
  103. package/apps/studio/test/sync-file-membership.test.ts +17 -1
  104. package/apps/studio/test/sync-file-plane.test.ts +976 -0
  105. package/apps/studio/test/sync-hub-listing.test.ts +46 -0
  106. package/apps/studio/test/sync-meta-codec.test.ts +76 -0
  107. package/apps/studio/test/sync-move-retirement.test.ts +231 -0
  108. package/apps/studio/test/sync-panel-surface.test.ts +20 -0
  109. package/apps/studio/test/sync-path-pull.test.ts +67 -1
  110. package/apps/studio/test/sync-pull-budget.test.ts +169 -0
  111. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
  112. package/apps/studio/test/sync-settings-routes.test.ts +195 -0
  113. package/apps/studio/test/sync-settings.test.ts +151 -0
  114. package/apps/studio/test/sync-status.test.ts +69 -0
  115. package/apps/studio/test/sync-trash.test.ts +132 -0
  116. package/apps/studio/test/workspace-containment.test.ts +45 -9
  117. package/apps/studio/tsconfig.json +9 -10
  118. package/apps/studio/use-annotation-resize.tsx +14 -3
  119. package/apps/studio/use-collab.tsx +3 -1
  120. package/apps/studio/whats-new.json +99 -0
  121. package/apps/studio/workspace-mode.ts +110 -62
  122. package/apps/studio/ws.ts +22 -1
  123. package/cli/bin/claude-design-server.mjs +19 -0
  124. package/cli/commands/design.mjs +25 -6
  125. package/cli/commands/hub-workspace.mjs +243 -22
  126. package/cli/commands/hub-workspace.test.mjs +171 -0
  127. package/cli/commands/hub.mjs +71 -1
  128. package/cli/lib/design-link.mjs +186 -1
  129. package/cli/lib/design-ownership.mjs +330 -0
  130. package/cli/lib/design-ownership.test.mjs +329 -0
  131. package/cli/lib/hubs-config.mjs +21 -0
  132. package/cli/lib/hubs-config.test.mjs +47 -1
  133. package/cli/lib/workspace-plan.mjs +298 -5
  134. package/cli/lib/workspace-plan.test.mjs +215 -1
  135. package/package.json +10 -10
  136. package/plugins/design/templates/_shell.html +43 -2
  137. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
  138. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  139. package/apps/studio/server.mjs +0 -1312
  140. package/apps/studio/sync/asset-pull.ts +0 -210
  141. package/apps/studio/sync/asset-push-worker.ts +0 -84
  142. package/apps/studio/sync/asset-sweep.ts +0 -262
  143. package/apps/studio/test/sync-asset-pull.test.ts +0 -161
  144. package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
  145. 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 = (await loadLog(historyScope || undefined)) || [];
439
+ const entries = await loadLog(historyScope || undefined);
421
440
  if (cancelled) return;
422
- setLog(entries);
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
- const branch = status?.branch || 'main';
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 className="gp-draft" title="Your project and shared draft">
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
- {project ? (
535
+ {headerProject ? (
482
536
  <>
483
- <b>{project}</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
- <h3>No saved versions yet</h3>
765
- <p>
766
- {previewable
767
- ? `Save a version of ${activeName} and it'll show up here.`
768
- : "Save a version and it'll show up here."}
769
- </p>
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, projectName }` when this is a cloud tab, else null. */
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
+ }