@brass-build/client 0.4.1 → 0.5.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/AGENTS.md +124 -45
- package/CHANGELOG.md +80 -4
- package/README.md +666 -179
- package/dist/api-fetch.d.ts +8 -0
- package/dist/api-fetch.d.ts.map +1 -1
- package/dist/api-fetch.js +35 -8
- package/dist/api-fetch.js.map +1 -1
- package/dist/batched-get.d.ts +11 -0
- package/dist/batched-get.d.ts.map +1 -0
- package/dist/batched-get.js +92 -0
- package/dist/batched-get.js.map +1 -0
- package/dist/call-service.d.ts +4 -0
- package/dist/call-service.d.ts.map +1 -1
- package/dist/call-service.js +66 -3
- package/dist/call-service.js.map +1 -1
- package/dist/client.d.ts +48 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +918 -115
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +25 -2
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +258 -61
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-commands.d.ts.map +1 -1
- package/dist/document-commands.js +5 -3
- package/dist/document-commands.js.map +1 -1
- package/dist/document-contents.d.ts +10 -0
- package/dist/document-contents.d.ts.map +1 -1
- package/dist/document-contents.js +23 -1
- package/dist/document-contents.js.map +1 -1
- package/dist/document-set-subscription.d.ts +56 -0
- package/dist/document-set-subscription.d.ts.map +1 -0
- package/dist/document-set-subscription.js +240 -0
- package/dist/document-set-subscription.js.map +1 -0
- package/dist/document-signals.d.ts +6 -0
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +31 -1
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +36 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +69 -18
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +12 -2
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +103 -27
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts +92 -10
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +285 -39
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +173 -8
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1778 -310
- package/dist/documents.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch-retry.d.ts +1 -0
- package/dist/fetch-retry.d.ts.map +1 -1
- package/dist/fetch-retry.js +2 -0
- package/dist/fetch-retry.js.map +1 -1
- package/dist/file-choice.d.ts +2 -0
- package/dist/file-choice.d.ts.map +1 -0
- package/dist/file-choice.js +27 -0
- package/dist/file-choice.js.map +1 -0
- package/dist/guest-link.d.ts +18 -1
- package/dist/guest-link.d.ts.map +1 -1
- package/dist/guest-link.js +329 -1
- package/dist/guest-link.js.map +1 -1
- package/dist/handle-types.d.ts +67 -5
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +15 -19
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +231 -76
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +61 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +94 -42
- package/dist/hosted-ui.js.map +1 -1
- package/dist/import-contract.d.ts +74 -0
- package/dist/import-contract.d.ts.map +1 -0
- package/dist/import-contract.js +163 -0
- package/dist/import-contract.js.map +1 -0
- package/dist/import-guest.d.ts +9 -0
- package/dist/import-guest.d.ts.map +1 -0
- package/dist/import-guest.js +155 -0
- package/dist/import-guest.js.map +1 -0
- package/dist/import-report.d.ts +17 -0
- package/dist/import-report.d.ts.map +1 -0
- package/dist/import-report.js +7 -0
- package/dist/import-report.js.map +1 -0
- package/dist/importer-wait.d.ts +2 -0
- package/dist/importer-wait.d.ts.map +1 -0
- package/dist/importer-wait.js +5 -0
- package/dist/importer-wait.js.map +1 -0
- package/dist/importer.d.ts +18 -4
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +322 -105
- package/dist/importer.js.map +1 -1
- package/dist/in-development.d.ts +2 -0
- package/dist/in-development.d.ts.map +1 -0
- package/dist/in-development.js +10 -0
- package/dist/in-development.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/init-keys.d.ts +1 -1
- package/dist/init-keys.d.ts.map +1 -1
- package/dist/init-keys.js +6 -1
- package/dist/init-keys.js.map +1 -1
- package/dist/input-documents.d.ts +67 -0
- package/dist/input-documents.d.ts.map +1 -0
- package/dist/input-documents.js +693 -0
- package/dist/input-documents.js.map +1 -0
- package/dist/inputs-status.d.ts +80 -0
- package/dist/inputs-status.d.ts.map +1 -0
- package/dist/inputs-status.js +460 -0
- package/dist/inputs-status.js.map +1 -0
- package/dist/inputs.d.ts +2 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +793 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/internal.js.map +1 -1
- package/dist/join-content.d.ts +33 -0
- package/dist/join-content.d.ts.map +1 -0
- package/dist/join-content.js +114 -0
- package/dist/join-content.js.map +1 -0
- package/dist/join-stamp.d.ts +3 -0
- package/dist/join-stamp.d.ts.map +1 -0
- package/dist/join-stamp.js +12 -0
- package/dist/join-stamp.js.map +1 -0
- package/dist/manifest-drift.d.ts +9 -0
- package/dist/manifest-drift.d.ts.map +1 -0
- package/dist/manifest-drift.js +46 -0
- package/dist/manifest-drift.js.map +1 -0
- package/dist/open-document.d.ts +109 -2
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +1822 -811
- package/dist/open-document.js.map +1 -1
- package/dist/page-restore.d.ts +12 -0
- package/dist/page-restore.d.ts.map +1 -0
- package/dist/page-restore.js +23 -0
- package/dist/page-restore.js.map +1 -0
- package/dist/placement.d.ts +13 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +34 -0
- package/dist/placement.js.map +1 -0
- package/dist/presence.d.ts +1 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +16 -4
- package/dist/presence.js.map +1 -1
- package/dist/read-only-document.d.ts +87 -0
- package/dist/read-only-document.d.ts.map +1 -0
- package/dist/read-only-document.js +4 -0
- package/dist/read-only-document.js.map +1 -0
- package/dist/record-paging.d.ts +63 -6
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +122 -29
- package/dist/record-paging.js.map +1 -1
- package/dist/references.d.ts +40 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +135 -0
- package/dist/references.js.map +1 -0
- package/dist/request-lane.d.ts +5 -0
- package/dist/request-lane.d.ts.map +1 -0
- package/dist/request-lane.js +48 -0
- package/dist/request-lane.js.map +1 -0
- package/dist/sealed-arrow.d.ts +170 -0
- package/dist/sealed-arrow.d.ts.map +1 -0
- package/dist/sealed-arrow.js +769 -0
- package/dist/sealed-arrow.js.map +1 -0
- package/dist/sealed-cache.d.ts +21 -0
- package/dist/sealed-cache.d.ts.map +1 -0
- package/dist/sealed-cache.js +247 -0
- package/dist/sealed-cache.js.map +1 -0
- package/dist/sealed-encode.d.ts +87 -0
- package/dist/sealed-encode.d.ts.map +1 -0
- package/dist/sealed-encode.js +876 -0
- package/dist/sealed-encode.js.map +1 -0
- package/dist/sealed-handles.d.ts +45 -0
- package/dist/sealed-handles.d.ts.map +1 -0
- package/dist/sealed-handles.js +408 -0
- package/dist/sealed-handles.js.map +1 -0
- package/dist/sealed-store.d.ts +242 -0
- package/dist/sealed-store.d.ts.map +1 -0
- package/dist/sealed-store.js +1155 -0
- package/dist/sealed-store.js.map +1 -0
- package/dist/serializer.d.ts +6 -15
- package/dist/serializer.d.ts.map +1 -1
- package/dist/session-toolbar.d.ts +20 -5
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +419 -67
- package/dist/session-toolbar.js.map +1 -1
- package/dist/session.d.ts +38 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -79
- package/dist/session.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +74 -0
- package/dist/sha256.js.map +1 -0
- package/dist/stable-handles.d.ts +19 -0
- package/dist/stable-handles.d.ts.map +1 -0
- package/dist/stable-handles.js +193 -0
- package/dist/stable-handles.js.map +1 -0
- package/dist/staged-seal.d.ts +2 -0
- package/dist/staged-seal.d.ts.map +1 -0
- package/dist/staged-seal.js +76 -0
- package/dist/staged-seal.js.map +1 -0
- package/dist/subscriber-delivery.d.ts +8 -0
- package/dist/subscriber-delivery.d.ts.map +1 -0
- package/dist/subscriber-delivery.js +31 -0
- package/dist/subscriber-delivery.js.map +1 -0
- package/dist/tail-op-import.d.ts +10 -0
- package/dist/tail-op-import.d.ts.map +1 -0
- package/dist/tail-op-import.js +33 -0
- package/dist/tail-op-import.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +6 -0
- package/dist/topic-lifecycle.d.ts.map +1 -1
- package/dist/topic-lifecycle.js +32 -2
- package/dist/topic-lifecycle.js.map +1 -1
- package/dist/trial-replace-dialog.d.ts +10 -0
- package/dist/trial-replace-dialog.d.ts.map +1 -0
- package/dist/trial-replace-dialog.js +153 -0
- package/dist/trial-replace-dialog.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +60 -5
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +344 -75
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts +1 -0
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +2 -1
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +21 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +192 -8
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts +1 -0
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +121 -17
- package/docs/custom-chrome.md +24 -16
- package/docs/hosted-ui-advanced.md +38 -6
- package/docs/imported-documents.md +104 -51
- package/docs/presence.md +16 -4
- package/docs/schemas.md +8 -5
- package/docs/services.md +6 -6
- package/docs/streams.md +139 -150
- package/docs/text-fields.md +26 -19
- package/package.json +5 -2
- package/src/api-fetch.ts +53 -3
- package/src/auth-flow.ts +1 -1
- package/src/batched-get.ts +117 -0
- package/src/call-service.ts +78 -3
- package/src/client.ts +1022 -112
- package/src/document-checkpoints.ts +307 -65
- package/src/document-commands.ts +5 -5
- package/src/document-contents.ts +27 -1
- package/src/document-set-subscription.ts +274 -0
- package/src/document-signals.ts +47 -5
- package/src/document-streams.ts +85 -16
- package/src/document-thumbnail-capture.ts +118 -32
- package/src/documents-namespace.ts +539 -84
- package/src/documents.ts +2017 -348
- package/src/errors.ts +17 -6
- package/src/fetch-retry.ts +5 -0
- package/src/file-choice.ts +26 -0
- package/src/guest-link.ts +362 -1
- package/src/handle-types.ts +73 -47
- package/src/handles.ts +253 -83
- package/src/hosted-ui.ts +211 -50
- package/src/import-contract.ts +227 -0
- package/src/import-guest.ts +193 -0
- package/src/import-report.ts +24 -0
- package/src/importer-wait.ts +4 -0
- package/src/importer.ts +386 -131
- package/src/in-development.ts +10 -0
- package/src/index.ts +76 -16
- package/src/init-keys.ts +6 -1
- package/src/input-documents.ts +819 -0
- package/src/inputs-status.ts +598 -0
- package/src/inputs.ts +1090 -0
- package/src/internal.ts +7 -0
- package/src/join-content.ts +135 -0
- package/src/join-stamp.ts +12 -0
- package/src/manifest-drift.ts +54 -0
- package/src/open-document.ts +2346 -930
- package/src/page-restore.ts +38 -0
- package/src/placement.ts +54 -0
- package/src/presence.ts +21 -5
- package/src/read-only-document.ts +133 -0
- package/src/record-paging.ts +147 -25
- package/src/references.ts +207 -0
- package/src/request-lane.ts +59 -0
- package/src/sealed-arrow.ts +843 -0
- package/src/sealed-cache.ts +267 -0
- package/src/sealed-encode.ts +1015 -0
- package/src/sealed-handles.ts +486 -0
- package/src/sealed-store.ts +1330 -0
- package/src/serializer.ts +8 -20
- package/src/session-toolbar.ts +463 -72
- package/src/session.ts +740 -99
- package/src/sha256.ts +65 -0
- package/src/stable-handles.ts +235 -0
- package/src/staged-seal.ts +90 -0
- package/src/subscriber-delivery.ts +37 -0
- package/src/tail-op-import.ts +30 -0
- package/src/topic-lifecycle.ts +38 -2
- package/src/trial-replace-dialog.ts +163 -0
- package/src/types.ts +6 -6
- package/src/undo.ts +386 -73
- package/src/version-bar.ts +5 -1
- package/src/version.ts +1 -1
- package/src/ws-client.ts +208 -16
- package/src/ws-frame-limit.ts +1 -0
package/dist/session-toolbar.js
CHANGED
|
@@ -23,12 +23,14 @@ const STYLE_ID = 'brass-toolbar-styles';
|
|
|
23
23
|
const CSS = `
|
|
24
24
|
.brass-ui { --brass-surface:#161b22; --brass-surface-2:#1f2730;
|
|
25
25
|
--brass-fg:#e6edf3; --brass-fg-muted:#9da7b3; --brass-border:#2a313b;
|
|
26
|
-
--brass-accent:#3b82f6; --brass-accent-fg:#fff; --brass-
|
|
26
|
+
--brass-accent:#3b82f6; --brass-accent-fg:#fff; --brass-accent-soft:#1c2b45;
|
|
27
|
+
--brass-danger:#e5534b;
|
|
27
28
|
color:var(--brass-fg);
|
|
28
29
|
font:14px system-ui,-apple-system,Segoe UI,Roboto,sans-serif; box-sizing:border-box; }
|
|
29
30
|
@media (prefers-color-scheme: light) { .brass-ui {
|
|
30
31
|
--brass-surface:#fff; --brass-surface-2:#f1f3f5; --brass-fg:#1b1f24;
|
|
31
|
-
--brass-fg-muted:#5c636a; --brass-border:#d8dee4; --brass-
|
|
32
|
+
--brass-fg-muted:#5c636a; --brass-border:#d8dee4; --brass-accent-soft:#e6effd;
|
|
33
|
+
--brass-danger:#cf222e; } }
|
|
32
34
|
.brass-ui *, .brass-bar * { box-sizing:border-box; }
|
|
33
35
|
/* Host apps commonly set a global button min-height for touch targets, which
|
|
34
36
|
overrides the avatar's height regardless of specificity and stretches the
|
|
@@ -61,6 +63,7 @@ const CSS = `
|
|
|
61
63
|
.brass-bar .b-tools { order:2; flex:1; flex-basis:auto;
|
|
62
64
|
justify-content:center; } .brass-bar .b-right { order:3; margin-left:auto; } }
|
|
63
65
|
.b-menu { display:inline-flex; }
|
|
66
|
+
.b-menu[hidden] { display:none; }
|
|
64
67
|
/* A bare hamburger glyph, no button chrome (background / border / radius), so it
|
|
65
68
|
reads as an icon sitting beside the document title. The 36px box is only an
|
|
66
69
|
invisible tap target; the glyph is sized up so it holds its weight next to the
|
|
@@ -88,6 +91,10 @@ const CSS = `
|
|
|
88
91
|
declared here. */
|
|
89
92
|
.b-pop .b-sep { height:0; border:none; border-top:1px solid var(--brass-border);
|
|
90
93
|
margin:4px 2px; }
|
|
94
|
+
/* A line of fact in a menu (when a document minted without an account
|
|
95
|
+
expires): the same inset as an entry, in the muted tone, and no hover. */
|
|
96
|
+
.b-pop .b-note { padding:8px 10px; color:var(--brass-fg-muted); font-size:12px;
|
|
97
|
+
line-height:1.3; }
|
|
91
98
|
/* [hidden] must beat the block rule above (the account menu swaps its body
|
|
92
99
|
between signed-in actions and the guest's "Sign in"). */
|
|
93
100
|
.b-pop button[hidden], .b-pop a[hidden], .b-pop .b-email[hidden],
|
|
@@ -113,7 +120,7 @@ const CSS = `
|
|
|
113
120
|
border:1px solid var(--brass-accent); border-radius:6px; padding:0 6px;
|
|
114
121
|
margin:0; cursor:pointer; font-family:inherit; overflow:hidden;
|
|
115
122
|
text-overflow:ellipsis; white-space:nowrap; }
|
|
116
|
-
.b-branch:hover { background:var(--brass-accent); color:var(--brass-accent-fg); }
|
|
123
|
+
.b-branch:hover:not(:disabled) { background:var(--brass-accent); color:var(--brass-accent-fg); }
|
|
117
124
|
.b-loc { font-size:11px; line-height:1.2; color:var(--brass-fg-muted);
|
|
118
125
|
background:none; border:none; border-radius:0; padding:1px 3px; min-height:0; cursor:pointer; white-space:nowrap;
|
|
119
126
|
font-family:inherit; }
|
|
@@ -121,7 +128,24 @@ const CSS = `
|
|
|
121
128
|
.b-loc.filed::before { content:'📁 '; }
|
|
122
129
|
.b-loc.loose { text-decoration:underline dashed; text-underline-offset:2px; }
|
|
123
130
|
.b-loc.loose::before { content:'+ '; }
|
|
124
|
-
.b-undo-redo { display:inline-flex; align-items:center; gap:
|
|
131
|
+
.b-undo-redo { display:inline-flex; align-items:center; gap:2px; }
|
|
132
|
+
.b-undo-compact { display:none; }
|
|
133
|
+
.b-tool.b-tool-icon { padding:6px; display:inline-flex; align-items:center; justify-content:center;
|
|
134
|
+
background:none; border-color:transparent; }
|
|
135
|
+
.b-tool.b-tool-icon:hover:not(:disabled) { background:var(--brass-surface-2); }
|
|
136
|
+
/* An icon button's name on hover or keyboard focus. A top-layer popover, so
|
|
137
|
+
no ancestor's overflow clips it. */
|
|
138
|
+
.b-tip { position:fixed; inset:auto; margin:0; padding:4px 8px; border:none;
|
|
139
|
+
border-radius:6px; background:var(--brass-fg); color:var(--brass-surface);
|
|
140
|
+
font-size:12px; line-height:1.3; white-space:nowrap; pointer-events:none;
|
|
141
|
+
display:none; z-index:2147483646; }
|
|
142
|
+
.b-tip:popover-open, .b-tip.b-tip-open { display:block; }
|
|
143
|
+
/* Narrow: the pair sits in the top row beside the avatar, so the tools row
|
|
144
|
+
holds only the app's items and collapses when it has none. */
|
|
145
|
+
@media (max-width:759.98px) {
|
|
146
|
+
.brass-bar .b-undo-compact { display:inline-flex; }
|
|
147
|
+
.brass-bar .b-undo-full { display:none; }
|
|
148
|
+
.brass-bar:not(.b-app-tools) .b-tools { display:none !important; } }
|
|
125
149
|
.b-tool { background:var(--brass-surface-2); color:var(--brass-fg);
|
|
126
150
|
border:1px solid var(--brass-border); border-radius:8px; padding:8px 12px; cursor:pointer; font:inherit; }
|
|
127
151
|
.b-tool:disabled { opacity:.5; cursor:default; }
|
|
@@ -143,6 +167,13 @@ const CSS = `
|
|
|
143
167
|
/* The uploaded avatar image fills the circle; object-fit:cover keeps it
|
|
144
168
|
undistorted. It sits over the monogram, which stays in the DOM as the
|
|
145
169
|
fallback shown when no image is set or the image fails to load. */
|
|
170
|
+
/* The accent-filled controls (this avatar, the guest strip's sign-in, the
|
|
171
|
+
branch chip) restate their fill and text colour on hover, at a
|
|
172
|
+
specificity above a host's own button:hover:not(:disabled) whatever order the
|
|
173
|
+
two stylesheets load in. A host hover rule otherwise repaints the fill with
|
|
174
|
+
its own (often near-white) background under this white text. */
|
|
175
|
+
button.b-avatar:hover:not(:disabled) { background:var(--brass-accent);
|
|
176
|
+
color:var(--brass-accent-fg); }
|
|
146
177
|
.b-avatar-img { width:100%; height:100%; object-fit:cover; display:block; }
|
|
147
178
|
.b-avatar-img[hidden], .b-avatar-initial[hidden] { display:none; }
|
|
148
179
|
/* The account menu opens on the identity it acts for: the avatar again at
|
|
@@ -210,6 +241,23 @@ const CSS = `
|
|
|
210
241
|
reports a problem, so it carries the danger tone rather than the muted one
|
|
211
242
|
every other line uses. */
|
|
212
243
|
.b-drive-unreadable { margin:0; padding:0 10px 6px; font-size:12px; color:var(--brass-danger, #b3261e); }
|
|
244
|
+
/* The strip under the bar while the open document is one the platform will
|
|
245
|
+
remove (created without an account): it says so and carries the sign-in
|
|
246
|
+
that keeps the work. Tinted with the accent so it reads as a notice about
|
|
247
|
+
the document rather than as more chrome; the text takes the slack and the
|
|
248
|
+
button holds the trailing edge until a narrow width wraps it under. */
|
|
249
|
+
.b-guest { width:100%; display:none; flex-wrap:wrap; align-items:center;
|
|
250
|
+
justify-content:space-between; gap:6px 12px; padding:8px 14px; font-size:13px;
|
|
251
|
+
line-height:1.4; background:var(--brass-accent-soft); color:var(--brass-fg);
|
|
252
|
+
border-bottom:1px solid var(--brass-border); }
|
|
253
|
+
.b-guest.b-guest-on { display:flex; }
|
|
254
|
+
.b-guest-msg { flex:1 1 auto; min-width:0; }
|
|
255
|
+
.b-guest button { flex:0 0 auto; background:var(--brass-accent);
|
|
256
|
+
color:var(--brass-accent-fg); border:none; border-radius:6px; cursor:pointer;
|
|
257
|
+
font:inherit; font-size:13px; font-weight:600; padding:6px 12px; }
|
|
258
|
+
.b-guest button:hover:not(:disabled) { background:var(--brass-accent);
|
|
259
|
+
color:var(--brass-accent-fg); filter:brightness(1.1); }
|
|
260
|
+
.b-guest button:focus-visible { outline:2px solid var(--brass-fg); outline-offset:2px; }
|
|
213
261
|
/* Historical-version preview bar: a full-width strip the toolbar shows below
|
|
214
262
|
the bar whenever the active doc is previewing an earlier version. The strip
|
|
215
263
|
itself is the platform-served version-bar iframe (the SDK owns only this
|
|
@@ -278,6 +326,12 @@ function el(tag, className) {
|
|
|
278
326
|
node.className = className;
|
|
279
327
|
return node;
|
|
280
328
|
}
|
|
329
|
+
// A failed publish shows the server's own sentence, which is what the route
|
|
330
|
+
// answers a refusal with.
|
|
331
|
+
function publishFailureText(err) {
|
|
332
|
+
const message = err instanceof Error ? err.message.trim() : '';
|
|
333
|
+
return message === '' || message === 'Request failed' ? 'Publishing failed.' : message;
|
|
334
|
+
}
|
|
281
335
|
function initialOf(user) {
|
|
282
336
|
const label = user.display_name ?? user.email ?? user.sub;
|
|
283
337
|
return label.trim().charAt(0).toUpperCase() || '?';
|
|
@@ -323,6 +377,26 @@ function renderCommandButton(cmd, asTool) {
|
|
|
323
377
|
b.disabled = !cmd.enabled || cmd.running;
|
|
324
378
|
return b;
|
|
325
379
|
}
|
|
380
|
+
const COMMAND_ICONS = {
|
|
381
|
+
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
382
|
+
redo: '<path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/>',
|
|
383
|
+
};
|
|
384
|
+
// A toolbar command as an icon button, named by its label for assistive
|
|
385
|
+
// technology and on hover. A command with no icon keeps its text.
|
|
386
|
+
function commandGlyph(icon) {
|
|
387
|
+
return `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`;
|
|
388
|
+
}
|
|
389
|
+
function renderCommandIcon(cmd) {
|
|
390
|
+
const icon = COMMAND_ICONS[cmd.id];
|
|
391
|
+
if (icon === undefined)
|
|
392
|
+
return renderCommandButton(cmd, true);
|
|
393
|
+
const b = el('button', 'b-tool b-tool-icon');
|
|
394
|
+
b.type = 'button';
|
|
395
|
+
b.setAttribute('aria-label', cmd.label);
|
|
396
|
+
b.innerHTML = commandGlyph(icon);
|
|
397
|
+
b.disabled = !cmd.enabled || cmd.running;
|
|
398
|
+
return b;
|
|
399
|
+
}
|
|
326
400
|
// Place a top-layer (position:fixed) popover under its anchor, clamped to the
|
|
327
401
|
// viewport so a wide one near an edge never runs off-screen. `align` picks the
|
|
328
402
|
// edge it lines up with the anchor: `start` (left) or `end` (right).
|
|
@@ -458,10 +532,20 @@ function buildFileMenu(deps) {
|
|
|
458
532
|
menu.trigger.textContent = '☰';
|
|
459
533
|
const render = () => {
|
|
460
534
|
const rows = [];
|
|
535
|
+
const expiresAt = deps.session.document?.expiresAt ?? null;
|
|
536
|
+
if (expiresAt !== null) {
|
|
537
|
+
const note = el('div', 'b-note b-expiry');
|
|
538
|
+
note.textContent = expiresInLabel(expiresAt);
|
|
539
|
+
rows.push(note);
|
|
540
|
+
}
|
|
461
541
|
let group = null;
|
|
462
542
|
for (const c of deps.session.commands) {
|
|
463
543
|
if (c.surface !== 'menu')
|
|
464
544
|
continue;
|
|
545
|
+
// A session command that is not enabled is not offered, so it is left
|
|
546
|
+
// out rather than greyed.
|
|
547
|
+
if (c.group === 'session' && !c.enabled)
|
|
548
|
+
continue;
|
|
465
549
|
// A rule wherever the family changes: the entries that put a
|
|
466
550
|
// different document on screen, then the ones about this document.
|
|
467
551
|
if (group !== null && c.group !== group) {
|
|
@@ -478,6 +562,9 @@ function buildFileMenu(deps) {
|
|
|
478
562
|
rows.push(b);
|
|
479
563
|
}
|
|
480
564
|
menu.pop.replaceChildren(...rows);
|
|
565
|
+
if (rows.length === 0)
|
|
566
|
+
menu.close();
|
|
567
|
+
menu.root.hidden = rows.length === 0;
|
|
481
568
|
};
|
|
482
569
|
render();
|
|
483
570
|
const unsub = deps.session.subscribe(render);
|
|
@@ -552,12 +639,12 @@ function buildDocumentTitle(deps) {
|
|
|
552
639
|
if (record.location_label !== null) {
|
|
553
640
|
loc.textContent = record.location_label;
|
|
554
641
|
loc.className = 'b-loc filed';
|
|
555
|
-
loc.title = 'Move or rename
|
|
642
|
+
loc.title = 'Move or rename';
|
|
556
643
|
}
|
|
557
644
|
else {
|
|
558
645
|
loc.textContent = 'Add to project';
|
|
559
646
|
loc.className = 'b-loc loose';
|
|
560
|
-
loc.title = '
|
|
647
|
+
loc.title = 'Add to a project';
|
|
561
648
|
}
|
|
562
649
|
loc.hidden = false;
|
|
563
650
|
}
|
|
@@ -580,15 +667,23 @@ function buildDocumentTitle(deps) {
|
|
|
580
667
|
// Three states, not two. A bound doc whose name has not resolved yet
|
|
581
668
|
// (a fresh create the server just numbered, a create/open still settling)
|
|
582
669
|
// and an open in flight (`status` 'loading', `document` still null) both
|
|
583
|
-
// leave the name UNKNOWN: render nothing rather than assert "
|
|
670
|
+
// leave the name UNKNOWN: render nothing rather than assert "Nothing open",
|
|
584
671
|
// the same way the session reports 'unknown' auth as a neutral state
|
|
585
672
|
// instead of flashing 'signed-out'. Only the genuine empty state (no doc,
|
|
586
|
-
// not loading) reads "
|
|
673
|
+
// not loading) reads "Nothing open".
|
|
674
|
+
// A link the app opens only for signed-in people names the refusal,
|
|
675
|
+
// since the account control beside it is where the visitor signs in.
|
|
676
|
+
const refusal = !doc &&
|
|
677
|
+
session.status === 'error' &&
|
|
678
|
+
session.error?.code === 'sign_in_required' &&
|
|
679
|
+
deps.onLink?.() === true
|
|
680
|
+
? session.error.message
|
|
681
|
+
: null;
|
|
587
682
|
const name = doc
|
|
588
683
|
? doc.name
|
|
589
684
|
: session.status === 'loading'
|
|
590
685
|
? null
|
|
591
|
-
: '
|
|
686
|
+
: (refusal ?? 'Nothing open');
|
|
592
687
|
if (name === null) {
|
|
593
688
|
title.hidden = true;
|
|
594
689
|
}
|
|
@@ -627,14 +722,97 @@ function buildDocumentTitle(deps) {
|
|
|
627
722
|
},
|
|
628
723
|
};
|
|
629
724
|
}
|
|
630
|
-
|
|
725
|
+
// Shows the aria-label of the icon button under the pointer or keyboard
|
|
726
|
+
// focus inside `root`, below the button and clamped to the viewport.
|
|
727
|
+
// One tip per document, outside every piece, so a piece's own children stay
|
|
728
|
+
// what it renders.
|
|
729
|
+
let sharedTip = null;
|
|
730
|
+
function iconTip() {
|
|
731
|
+
if (sharedTip?.isConnected)
|
|
732
|
+
return sharedTip;
|
|
733
|
+
sharedTip = el('div', 'brass-ui b-tip');
|
|
734
|
+
sharedTip.setAttribute('aria-hidden', 'true');
|
|
735
|
+
sharedTip.setAttribute('popover', 'manual');
|
|
736
|
+
document.body.append(sharedTip);
|
|
737
|
+
return sharedTip;
|
|
738
|
+
}
|
|
739
|
+
function attachIconTooltips(root) {
|
|
740
|
+
const tip = iconTip();
|
|
741
|
+
const api = tip;
|
|
742
|
+
let shownFor = null;
|
|
743
|
+
const show = (btn) => {
|
|
744
|
+
const label = btn.getAttribute('aria-label');
|
|
745
|
+
if (!label)
|
|
746
|
+
return;
|
|
747
|
+
shownFor = btn;
|
|
748
|
+
tip.textContent = label;
|
|
749
|
+
if (typeof api.showPopover === 'function') {
|
|
750
|
+
try {
|
|
751
|
+
if (!tip.matches(':popover-open'))
|
|
752
|
+
api.showPopover();
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
tip.classList.add('b-tip-open');
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else
|
|
759
|
+
tip.classList.add('b-tip-open');
|
|
760
|
+
const r = btn.getBoundingClientRect();
|
|
761
|
+
const w = tip.offsetWidth;
|
|
762
|
+
const left = Math.max(4, Math.min(r.left + r.width / 2 - w / 2, window.innerWidth - w - 4));
|
|
763
|
+
tip.style.left = `${left}px`;
|
|
764
|
+
tip.style.top = `${r.bottom + 6}px`;
|
|
765
|
+
};
|
|
766
|
+
const hide = () => {
|
|
767
|
+
shownFor = null;
|
|
768
|
+
tip.classList.remove('b-tip-open');
|
|
769
|
+
try {
|
|
770
|
+
if (tip.matches(':popover-open'))
|
|
771
|
+
api.hidePopover?.();
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Not showing.
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const iconOf = (t) => t instanceof Element ? t.closest('.b-tool-icon') : null;
|
|
778
|
+
root.addEventListener('pointerover', (e) => {
|
|
779
|
+
const btn = iconOf(e.target);
|
|
780
|
+
if (btn && e.pointerType === 'mouse')
|
|
781
|
+
show(btn);
|
|
782
|
+
});
|
|
783
|
+
root.addEventListener('pointerout', (e) => {
|
|
784
|
+
if (iconOf(e.target) === shownFor && iconOf(e.relatedTarget) !== shownFor)
|
|
785
|
+
hide();
|
|
786
|
+
});
|
|
787
|
+
root.addEventListener('pointerdown', hide);
|
|
788
|
+
root.addEventListener('focusin', (e) => {
|
|
789
|
+
const btn = iconOf(e.target);
|
|
790
|
+
if (btn && btn.matches(':focus-visible'))
|
|
791
|
+
show(btn);
|
|
792
|
+
});
|
|
793
|
+
root.addEventListener('focusout', hide);
|
|
794
|
+
return { hide };
|
|
795
|
+
}
|
|
796
|
+
// `full` and `compact` are the toolbar's two copies, which the narrow-width
|
|
797
|
+
// rules swap. `standalone` is the one an app mounts itself: no copy stands in
|
|
798
|
+
// for it at a narrow width, so those rules must not hide it.
|
|
799
|
+
function buildUndoRedo(deps, variant = 'full') {
|
|
631
800
|
injectStyles(document);
|
|
632
|
-
const root = el('div', 'brass-ui b-undo-redo');
|
|
801
|
+
const root = el('div', variant === 'standalone' ? 'brass-ui b-undo-redo' : `brass-ui b-undo-redo b-undo-${variant}`);
|
|
802
|
+
const tips = attachIconTooltips(root);
|
|
633
803
|
const render = () => {
|
|
634
|
-
|
|
635
|
-
|
|
804
|
+
// The pair shows only while one of them can act: before the first edit, on
|
|
805
|
+
// a read-only document, and with nothing open they would only ever show
|
|
806
|
+
// disabled, and rendering none lets the bar give their row back.
|
|
807
|
+
const commands = deps.session.commands.filter((c) => c.surface === 'toolbar');
|
|
808
|
+
tips.hide();
|
|
809
|
+
if (!commands.some((c) => c.enabled)) {
|
|
810
|
+
root.replaceChildren();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
root.replaceChildren(...commands
|
|
636
814
|
.map((c) => {
|
|
637
|
-
const b =
|
|
815
|
+
const b = renderCommandIcon(c);
|
|
638
816
|
b.onclick = () => void c.run();
|
|
639
817
|
return b;
|
|
640
818
|
}));
|
|
@@ -645,6 +823,7 @@ function buildUndoRedo(deps) {
|
|
|
645
823
|
root,
|
|
646
824
|
destroy() {
|
|
647
825
|
unsub();
|
|
826
|
+
tips.hide();
|
|
648
827
|
},
|
|
649
828
|
};
|
|
650
829
|
}
|
|
@@ -658,8 +837,26 @@ function buildPresenceRoster(deps) {
|
|
|
658
837
|
// people as present on the one they are looking at.
|
|
659
838
|
let boundDoc = null;
|
|
660
839
|
let presenceOff = null;
|
|
840
|
+
// The roster lists every other connection, so the reader's own other tabs,
|
|
841
|
+
// and a page they navigated away from until its socket closes, are in it.
|
|
842
|
+
// A pip stands for a person: one per sub, and never the reader. A visitor
|
|
843
|
+
// on a shared link has an empty sub, so each such connection is its own
|
|
844
|
+
// pip.
|
|
845
|
+
const collaborators = (participants) => {
|
|
846
|
+
const own = deps.getUser?.()?.sub;
|
|
847
|
+
const seen = new Set();
|
|
848
|
+
return participants.filter((p) => {
|
|
849
|
+
if (own !== undefined && p.sub === own)
|
|
850
|
+
return false;
|
|
851
|
+
const person = p.sub !== '' ? p.sub : `connection:${p.connectionId}`;
|
|
852
|
+
if (seen.has(person))
|
|
853
|
+
return false;
|
|
854
|
+
seen.add(person);
|
|
855
|
+
return true;
|
|
856
|
+
});
|
|
857
|
+
};
|
|
661
858
|
const renderRoster = (participants) => {
|
|
662
|
-
root.replaceChildren(...participants.map((p) => {
|
|
859
|
+
root.replaceChildren(...collaborators(participants).map((p) => {
|
|
663
860
|
const pip = el('span', 'b-pip');
|
|
664
861
|
pip.style.background = p.color;
|
|
665
862
|
const label = p.displayName ?? p.sub ?? '?';
|
|
@@ -714,6 +911,28 @@ function buildPresenceRoster(deps) {
|
|
|
714
911
|
},
|
|
715
912
|
};
|
|
716
913
|
}
|
|
914
|
+
// The guest's account-menu entry, and its wording on a document created
|
|
915
|
+
// without an account, where signing in is what keeps it.
|
|
916
|
+
const SIGN_IN_LABEL = 'Sign in';
|
|
917
|
+
const SIGN_IN_TO_KEEP_LABEL = 'Sign in to keep your work';
|
|
918
|
+
const KEEP_LABEL = 'Keep';
|
|
919
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
920
|
+
// The file menu's line for a document the platform will remove: whole days
|
|
921
|
+
// until then, rounded up, so the last day still reads as one.
|
|
922
|
+
export function expiresInLabel(expiresAt, now = Date.now()) {
|
|
923
|
+
const days = Math.max(1, Math.ceil((expiresAt.getTime() - now) / DAY_MS));
|
|
924
|
+
return `Your work will be lost in ${days} ${days === 1 ? 'day' : 'days'}`;
|
|
925
|
+
}
|
|
926
|
+
// Someone else's temporary work, to a signed-in reader who cannot keep it.
|
|
927
|
+
export function removedInLabel(expiresAt, now = Date.now()) {
|
|
928
|
+
const days = Math.max(1, Math.ceil((expiresAt.getTime() - now) / DAY_MS));
|
|
929
|
+
return `This work will be removed in ${days} ${days === 1 ? 'day' : 'days'}`;
|
|
930
|
+
}
|
|
931
|
+
// The strip's sentence: the same count as the file menu's line, led by the
|
|
932
|
+
// state that explains it.
|
|
933
|
+
export function notSignedInLabel(expiresAt, now = Date.now()) {
|
|
934
|
+
return `You are not signed in. ${expiresInLabel(expiresAt, now)}.`;
|
|
935
|
+
}
|
|
717
936
|
// A monochrome 16px stroke glyph (inherits `color` via `currentColor`). Built
|
|
718
937
|
// in the SVG namespace because `createElement('svg')` yields an HTML element
|
|
719
938
|
// that won't render.
|
|
@@ -803,13 +1022,23 @@ function buildDriveSync(deps) {
|
|
|
803
1022
|
// covering the gap between a click here and the first `publishing` frame
|
|
804
1023
|
// arriving; the two combine as `isPublishing` in `render`.
|
|
805
1024
|
let publishing = false;
|
|
806
|
-
|
|
1025
|
+
// The server's refusal of the last publish from this panel, shown where
|
|
1026
|
+
// the resting sentence sits until the next attempt.
|
|
1027
|
+
let publishError = null;
|
|
807
1028
|
let publishTimer = null;
|
|
808
1029
|
// Set on a click, cleared by the next publish-state frame. Between the two,
|
|
809
1030
|
// the held state describes the PREVIOUS attempt, and rendering it would
|
|
810
1031
|
// report the last failure over the publish just started.
|
|
811
1032
|
let publishStateStale = false;
|
|
812
1033
|
let currentStatus = null;
|
|
1034
|
+
// The panel is asking whether to publish over the drive's newer version.
|
|
1035
|
+
let confirmingOverwrite = false;
|
|
1036
|
+
pop.addEventListener('toggle', (event) => {
|
|
1037
|
+
if (event.newState === 'closed' && confirmingOverwrite) {
|
|
1038
|
+
confirmingOverwrite = false;
|
|
1039
|
+
render(currentStatus);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
813
1042
|
const clearPublishing = () => {
|
|
814
1043
|
publishing = false;
|
|
815
1044
|
// The stale mark is cleared with the hold it belongs to. It is otherwise
|
|
@@ -829,6 +1058,16 @@ function buildDriveSync(deps) {
|
|
|
829
1058
|
// one.
|
|
830
1059
|
const renderPublishOffer = (target) => {
|
|
831
1060
|
root.hidden = false;
|
|
1061
|
+
if (target.unpublishable) {
|
|
1062
|
+
// Nothing to publish, so the icon asks for no attention; the panel
|
|
1063
|
+
// names the drive and says why the file cannot go to it.
|
|
1064
|
+
root.classList.remove('has-changes', 'is-broken');
|
|
1065
|
+
button.title = `Cannot publish to ${target.driveName}`;
|
|
1066
|
+
const p = el('p', 'b-drive-unreadable');
|
|
1067
|
+
p.textContent = target.detail;
|
|
1068
|
+
pop.replaceChildren(p);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
832
1071
|
// A document the drive has never received is as far out of sync as one
|
|
833
1072
|
// whose edits have not been published, so the icon carries the same
|
|
834
1073
|
// attention an `ahead` document's does. What separates them is what the
|
|
@@ -842,7 +1081,8 @@ function buildDriveSync(deps) {
|
|
|
842
1081
|
: state?.failed
|
|
843
1082
|
? `Publish to ${target.driveName} failed`
|
|
844
1083
|
: `Publish to ${target.driveName}`;
|
|
845
|
-
const failed = !inFlight &&
|
|
1084
|
+
const failed = !inFlight &&
|
|
1085
|
+
(publishError !== null || state?.failed === true || target.publishFailed === true);
|
|
846
1086
|
const rows = [];
|
|
847
1087
|
// One line, server-authored either way: the worker's phase copy while it
|
|
848
1088
|
// runs, its account of the failure when it stopped, and the resting
|
|
@@ -852,21 +1092,21 @@ function buildDriveSync(deps) {
|
|
|
852
1092
|
const p = el('p', failed ? 'b-drive-unreadable' : 'b-drive-detail');
|
|
853
1093
|
p.textContent = inFlight
|
|
854
1094
|
? (state?.publishing === true ? (state.detail ?? 'Publishing...') : 'Publishing...')
|
|
855
|
-
: (state?.detail ?? target.detail);
|
|
1095
|
+
: (publishError ?? state?.detail ?? target.detail);
|
|
856
1096
|
rows.push(p);
|
|
857
1097
|
const pub = el('button');
|
|
858
1098
|
pub.type = 'button';
|
|
859
1099
|
pub.disabled = inFlight;
|
|
860
1100
|
pub.textContent = inFlight
|
|
861
1101
|
? 'Publishing...'
|
|
862
|
-
:
|
|
1102
|
+
: failed
|
|
863
1103
|
? `Publish to ${target.driveName}, retry`
|
|
864
1104
|
: `Publish to ${target.driveName}`;
|
|
865
1105
|
pub.onclick = () => {
|
|
866
1106
|
const doc = deps.session.document;
|
|
867
1107
|
if (!doc)
|
|
868
1108
|
return;
|
|
869
|
-
publishError =
|
|
1109
|
+
publishError = null;
|
|
870
1110
|
publishing = true;
|
|
871
1111
|
publishStateStale = true;
|
|
872
1112
|
// The same safety net the mirror panel keeps: a publish that neither
|
|
@@ -877,9 +1117,9 @@ function buildDriveSync(deps) {
|
|
|
877
1117
|
render(currentStatus);
|
|
878
1118
|
}, PUBLISH_HOLD_MS);
|
|
879
1119
|
render(currentStatus);
|
|
880
|
-
void doc.pushToDrive().catch(() => {
|
|
1120
|
+
void doc.pushToDrive().catch((err) => {
|
|
881
1121
|
clearPublishing();
|
|
882
|
-
publishError =
|
|
1122
|
+
publishError = publishFailureText(err);
|
|
883
1123
|
render(currentStatus);
|
|
884
1124
|
});
|
|
885
1125
|
};
|
|
@@ -888,6 +1128,8 @@ function buildDriveSync(deps) {
|
|
|
888
1128
|
};
|
|
889
1129
|
const render = (status) => {
|
|
890
1130
|
currentStatus = status;
|
|
1131
|
+
if (status?.state !== 'diverged' || !status.canPush)
|
|
1132
|
+
confirmingOverwrite = false;
|
|
891
1133
|
// A document that mirrors nothing can still have somewhere to go: its
|
|
892
1134
|
// project may be linked to a drive that takes it as a new file, which is
|
|
893
1135
|
// what an upload into a linked project sits waiting for. Offer that
|
|
@@ -910,7 +1152,7 @@ function buildDriveSync(deps) {
|
|
|
910
1152
|
// local hold (the server frame already has `publishing: false`).
|
|
911
1153
|
if (status.state === 'synced') {
|
|
912
1154
|
clearPublishing();
|
|
913
|
-
publishError =
|
|
1155
|
+
publishError = null;
|
|
914
1156
|
}
|
|
915
1157
|
root.hidden = false;
|
|
916
1158
|
// Either a local click hold or the server's own `publishing` flag means a
|
|
@@ -973,6 +1215,11 @@ function buildDriveSync(deps) {
|
|
|
973
1215
|
warn.textContent = status.sourceUnreadable;
|
|
974
1216
|
rows.push(warn);
|
|
975
1217
|
}
|
|
1218
|
+
if (publishError !== null && !isPublishing) {
|
|
1219
|
+
const warn = el('p', 'b-drive-unreadable');
|
|
1220
|
+
warn.textContent = publishError;
|
|
1221
|
+
rows.push(warn);
|
|
1222
|
+
}
|
|
976
1223
|
if (isPublishing) {
|
|
977
1224
|
// Held while the publish is in flight (a local click hold, or the doc's
|
|
978
1225
|
// pending work-status), until it reaches synced (see `isPublishing`).
|
|
@@ -983,17 +1230,14 @@ function buildDriveSync(deps) {
|
|
|
983
1230
|
rows.push(pub);
|
|
984
1231
|
}
|
|
985
1232
|
else if (status.canPush) {
|
|
986
|
-
// `canPush`
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
const pub = el('button');
|
|
990
|
-
pub.type = 'button';
|
|
991
|
-
pub.textContent = publishError ? 'Publish failed, retry' : 'Publish changes';
|
|
992
|
-
pub.onclick = () => {
|
|
1233
|
+
// `canPush` is the server's publish decision, so this never offers a
|
|
1234
|
+
// publish the server would refuse.
|
|
1235
|
+
const startPublish = () => {
|
|
993
1236
|
const doc = deps.session.document;
|
|
994
1237
|
if (!doc)
|
|
995
1238
|
return;
|
|
996
|
-
|
|
1239
|
+
confirmingOverwrite = false;
|
|
1240
|
+
publishError = null;
|
|
997
1241
|
publishing = true;
|
|
998
1242
|
// Safety net: if the publish neither errors nor reaches synced (the
|
|
999
1243
|
// webhook never confirms), release the held state so the popover
|
|
@@ -1003,12 +1247,45 @@ function buildDriveSync(deps) {
|
|
|
1003
1247
|
render(currentStatus);
|
|
1004
1248
|
}, PUBLISH_HOLD_MS);
|
|
1005
1249
|
render(currentStatus);
|
|
1006
|
-
void doc.pushToDrive().catch(() => {
|
|
1250
|
+
void doc.pushToDrive().catch((err) => {
|
|
1007
1251
|
clearPublishing();
|
|
1008
|
-
publishError =
|
|
1252
|
+
publishError = publishFailureText(err);
|
|
1009
1253
|
render(currentStatus);
|
|
1010
1254
|
});
|
|
1011
1255
|
};
|
|
1256
|
+
// A diverged document publishes over a drive version it never took, so
|
|
1257
|
+
// the panel asks first, in place.
|
|
1258
|
+
const overwrites = status.state === 'diverged';
|
|
1259
|
+
if (overwrites && confirmingOverwrite) {
|
|
1260
|
+
const ask = el('p', 'b-drive-unreadable');
|
|
1261
|
+
ask.textContent = status.keepsPriorVersions
|
|
1262
|
+
? `Publishing makes this doc the latest version in ${driveName}. ${driveName} keeps the newer version in its history.`
|
|
1263
|
+
: `Publishing replaces the newer version in ${driveName} with this doc.`;
|
|
1264
|
+
const confirm = el('button');
|
|
1265
|
+
confirm.type = 'button';
|
|
1266
|
+
confirm.textContent = `Publish over ${driveName}`;
|
|
1267
|
+
confirm.onclick = startPublish;
|
|
1268
|
+
const cancel = el('button');
|
|
1269
|
+
cancel.type = 'button';
|
|
1270
|
+
cancel.textContent = 'Cancel';
|
|
1271
|
+
cancel.onclick = () => {
|
|
1272
|
+
confirmingOverwrite = false;
|
|
1273
|
+
render(currentStatus);
|
|
1274
|
+
};
|
|
1275
|
+
rows.push(ask, confirm, cancel);
|
|
1276
|
+
pop.replaceChildren(...rows);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const pub = el('button');
|
|
1280
|
+
pub.type = 'button';
|
|
1281
|
+
const verb = publishError !== null ? 'Publish failed, retry' : 'Publish changes';
|
|
1282
|
+
pub.textContent = overwrites ? `${verb}...` : verb;
|
|
1283
|
+
pub.onclick = overwrites
|
|
1284
|
+
? () => {
|
|
1285
|
+
confirmingOverwrite = true;
|
|
1286
|
+
render(currentStatus);
|
|
1287
|
+
}
|
|
1288
|
+
: startPublish;
|
|
1012
1289
|
rows.push(pub);
|
|
1013
1290
|
}
|
|
1014
1291
|
// Resolve a divergence via the hosted take-source / keep-both surface. Not
|
|
@@ -1106,7 +1383,6 @@ function buildAccountMenu(deps) {
|
|
|
1106
1383
|
// landing and stands in when the app is working in none.
|
|
1107
1384
|
const dashboardLink = el('a');
|
|
1108
1385
|
dashboardLink.append(strokeIcon('M14 3h7v7', 'M10 14 21 3', 'M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5'), 'Open Brass dashboard');
|
|
1109
|
-
dashboardLink.href = `${deps.dashboardBaseUrl}/home`;
|
|
1110
1386
|
dashboardLink.target = '_blank';
|
|
1111
1387
|
dashboardLink.rel = 'noopener noreferrer';
|
|
1112
1388
|
dashboardLink.onclick = () => menu.close();
|
|
@@ -1130,7 +1406,8 @@ function buildAccountMenu(deps) {
|
|
|
1130
1406
|
// meaningless, so the menu offers a single "Sign in" that takes the top
|
|
1131
1407
|
// window through the app's normal sign-in and returns to this URL.
|
|
1132
1408
|
const signInBtn = el('button');
|
|
1133
|
-
|
|
1409
|
+
const signInLabel = document.createTextNode(SIGN_IN_LABEL);
|
|
1410
|
+
signInBtn.append(strokeIcon('M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4', 'm10 17 5-5-5-5', 'M15 12H3'), signInLabel);
|
|
1134
1411
|
signInBtn.onclick = () => {
|
|
1135
1412
|
menu.close();
|
|
1136
1413
|
deps.signIn();
|
|
@@ -1161,6 +1438,11 @@ function buildAccountMenu(deps) {
|
|
|
1161
1438
|
dashboardLink.hidden = !signedIn;
|
|
1162
1439
|
signOutBtn.hidden = !signedIn;
|
|
1163
1440
|
signInBtn.hidden = signedIn;
|
|
1441
|
+
// On a document that expires, signing in is what keeps it.
|
|
1442
|
+
signInLabel.data =
|
|
1443
|
+
deps.session?.document?.expiresAt != null
|
|
1444
|
+
? SIGN_IN_TO_KEEP_LABEL
|
|
1445
|
+
: SIGN_IN_LABEL;
|
|
1164
1446
|
// The organization line names what the app is acting in, so it is drawn
|
|
1165
1447
|
// only when there is something to name: an app that has not needed an
|
|
1166
1448
|
// organization yet has not resolved one, and resolving one to fill this
|
|
@@ -1169,10 +1451,11 @@ function buildAccountMenu(deps) {
|
|
|
1169
1451
|
orgEl.textContent = organizationName;
|
|
1170
1452
|
orgEl.title = organizationName;
|
|
1171
1453
|
orgEl.hidden = organizationName === '';
|
|
1454
|
+
const dashboardBaseUrl = deps.dashboardBaseUrl();
|
|
1172
1455
|
dashboardLink.href =
|
|
1173
1456
|
signedIn && organization
|
|
1174
|
-
? `${
|
|
1175
|
-
: `${
|
|
1457
|
+
? `${dashboardBaseUrl}/organizations/${encodeURIComponent(organization.id)}`
|
|
1458
|
+
: `${dashboardBaseUrl}/home`;
|
|
1176
1459
|
// Offered to every signed-in reader, including one the line has nothing
|
|
1177
1460
|
// to name and one who belongs to a single organization. The surface
|
|
1178
1461
|
// behind it both moves the app and creates an organization, so gating it
|
|
@@ -1190,18 +1473,70 @@ function buildAccountMenu(deps) {
|
|
|
1190
1473
|
organization = next;
|
|
1191
1474
|
render();
|
|
1192
1475
|
});
|
|
1476
|
+
const unsubSession = deps.session?.subscribe(render);
|
|
1193
1477
|
return {
|
|
1194
1478
|
root: menu.root,
|
|
1195
1479
|
destroy() {
|
|
1196
1480
|
unsub();
|
|
1197
1481
|
unsubOrganization?.();
|
|
1482
|
+
unsubSession?.();
|
|
1198
1483
|
menu.destroy();
|
|
1199
1484
|
},
|
|
1200
1485
|
};
|
|
1201
1486
|
}
|
|
1487
|
+
// The strip under the bar on a document created without an account. It is
|
|
1488
|
+
// drawn whenever the active doc carries `expiresAt`, re-read on every session
|
|
1489
|
+
// notification: a sign-in claims the document and clears the expiry, and the
|
|
1490
|
+
// active document can change under the chrome. Empty otherwise, so the row
|
|
1491
|
+
// costs no height on an ordinary document.
|
|
1492
|
+
function buildNotSignedIn(deps) {
|
|
1493
|
+
injectStyles(document);
|
|
1494
|
+
const root = el('div', 'brass-ui b-guest');
|
|
1495
|
+
root.setAttribute('role', 'status');
|
|
1496
|
+
const message = el('span', 'b-guest-msg');
|
|
1497
|
+
const signInBtn = el('button');
|
|
1498
|
+
signInBtn.type = 'button';
|
|
1499
|
+
signInBtn.textContent = SIGN_IN_LABEL;
|
|
1500
|
+
signInBtn.onclick = () => deps.signIn();
|
|
1501
|
+
const keepBtn = el('button');
|
|
1502
|
+
keepBtn.type = 'button';
|
|
1503
|
+
keepBtn.textContent = KEEP_LABEL;
|
|
1504
|
+
keepBtn.onclick = () => deps.keep?.();
|
|
1505
|
+
const render = () => {
|
|
1506
|
+
const expiresAt = deps.session.document?.expiresAt ?? null;
|
|
1507
|
+
if (expiresAt === null) {
|
|
1508
|
+
root.classList.remove('b-guest-on');
|
|
1509
|
+
root.replaceChildren();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if ((deps.getUser?.() ?? null) === null) {
|
|
1513
|
+
message.textContent = notSignedInLabel(expiresAt);
|
|
1514
|
+
root.replaceChildren(message, signInBtn);
|
|
1515
|
+
}
|
|
1516
|
+
else if (deps.canKeep?.() === true && deps.keep !== undefined) {
|
|
1517
|
+
message.textContent = `${expiresInLabel(expiresAt)}.`;
|
|
1518
|
+
root.replaceChildren(message, keepBtn);
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
message.textContent = `${removedInLabel(expiresAt)}.`;
|
|
1522
|
+
root.replaceChildren(message);
|
|
1523
|
+
}
|
|
1524
|
+
root.classList.add('b-guest-on');
|
|
1525
|
+
};
|
|
1526
|
+
render();
|
|
1527
|
+
const unsub = deps.session.subscribe(render);
|
|
1528
|
+
const unsubUser = deps.onUserChange?.(() => render());
|
|
1529
|
+
return {
|
|
1530
|
+
root,
|
|
1531
|
+
destroy() {
|
|
1532
|
+
unsub();
|
|
1533
|
+
unsubUser?.();
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1202
1537
|
// The historical-version preview bar. When the active doc is previewing an
|
|
1203
1538
|
// earlier version (a hosted `history`-initiated checkout: `doc.readOnly` is
|
|
1204
|
-
// true and `doc.
|
|
1539
|
+
// true and `doc.previewVersion` is set), the SDK mounts the platform-served
|
|
1205
1540
|
// `/embed/version-bar` inline as a strip and routes its intents (Prev / Next
|
|
1206
1541
|
// checkout, return-to-latest, browse, branch-from) to the doc's internal
|
|
1207
1542
|
// methods. Delivered as a server embed so the platform owns and grows the
|
|
@@ -1212,7 +1547,7 @@ function buildVersionPreview(deps) {
|
|
|
1212
1547
|
injectStyles(document);
|
|
1213
1548
|
const root = el('div', 'brass-ui b-version');
|
|
1214
1549
|
root.setAttribute('role', 'group');
|
|
1215
|
-
root.setAttribute('aria-label', '
|
|
1550
|
+
root.setAttribute('aria-label', 'Version');
|
|
1216
1551
|
let frame = null;
|
|
1217
1552
|
// Once a preview starts (a checkout), the bar owns a browsing session that
|
|
1218
1553
|
// stays open, including after returning to the live head, until the user
|
|
@@ -1224,7 +1559,7 @@ function buildVersionPreview(deps) {
|
|
|
1224
1559
|
// The version the reader dismissed the bar at, when the document was left
|
|
1225
1560
|
// holding one. A document that cannot return to its head (one pinned to a
|
|
1226
1561
|
// version) stays on it, so without this the next render reads a non-null
|
|
1227
|
-
// `doc.
|
|
1562
|
+
// `doc.previewVersion` and latches the bar the reader just dismissed straight back
|
|
1228
1563
|
// on. A dismissal at the live head records nothing: there is no version
|
|
1229
1564
|
// left to latch on, and the head is where the next browse-in-the-app
|
|
1230
1565
|
// request arrives, so a record there would swallow it.
|
|
@@ -1250,12 +1585,15 @@ function buildVersionPreview(deps) {
|
|
|
1250
1585
|
throw err;
|
|
1251
1586
|
}
|
|
1252
1587
|
};
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1588
|
+
// Assembled when the bar opens rather than at mount, so the frame's origin
|
|
1589
|
+
// and theme are the ones `Brass.init()` settled (see `AccountMenuDeps`).
|
|
1590
|
+
const frameDeps = () => ({
|
|
1591
|
+
dashboardBaseUrl: deps.dashboardBaseUrl(),
|
|
1592
|
+
appId: deps.appId?.(),
|
|
1593
|
+
...(deps.hostedUiTheme ? { theme: deps.hostedUiTheme() } : {}),
|
|
1256
1594
|
// Read the live active document at call time: a bridge intent lands
|
|
1257
1595
|
// async, so it must not close over a doc that may have been superseded.
|
|
1258
|
-
currentVersion: () => session.document?.
|
|
1596
|
+
currentVersion: () => session.document?.previewVersion ?? null,
|
|
1259
1597
|
checkoutVersion: async (version) => {
|
|
1260
1598
|
const doc = session.document;
|
|
1261
1599
|
if (!doc)
|
|
@@ -1286,7 +1624,7 @@ function buildVersionPreview(deps) {
|
|
|
1286
1624
|
// flag is cleared too, or the next render would latch the bar back on.
|
|
1287
1625
|
const doc = session.document;
|
|
1288
1626
|
browsing = false;
|
|
1289
|
-
dismissedAt = doc?.
|
|
1627
|
+
dismissedAt = doc?.previewVersion ?? undefined;
|
|
1290
1628
|
doc?._setVersionBrowsing(false);
|
|
1291
1629
|
// Leaving the bar means leaving the preview: the bar is the only
|
|
1292
1630
|
// surface that returns a document to its head, so a dismissal that
|
|
@@ -1299,7 +1637,7 @@ function buildVersionPreview(deps) {
|
|
|
1299
1637
|
// pick sets only `version`, so a stale `dismissedAt` equal to it would
|
|
1300
1638
|
// otherwise leave the reader checked out, read-only, with no bar and
|
|
1301
1639
|
// no exit).
|
|
1302
|
-
if (doc && doc.
|
|
1640
|
+
if (doc && doc.previewVersion !== null) {
|
|
1303
1641
|
void attempt(doc, () => doc.returnToHead())
|
|
1304
1642
|
.then(() => {
|
|
1305
1643
|
if (session.document === doc)
|
|
@@ -1309,7 +1647,7 @@ function buildVersionPreview(deps) {
|
|
|
1309
1647
|
}
|
|
1310
1648
|
hideBar();
|
|
1311
1649
|
},
|
|
1312
|
-
};
|
|
1650
|
+
});
|
|
1313
1651
|
const render = () => {
|
|
1314
1652
|
const doc = session.document;
|
|
1315
1653
|
const docId = doc?.id ?? null;
|
|
@@ -1322,7 +1660,7 @@ function buildVersionPreview(deps) {
|
|
|
1322
1660
|
browsing = false;
|
|
1323
1661
|
dismissedAt = undefined;
|
|
1324
1662
|
}
|
|
1325
|
-
// A live checkout (`doc.
|
|
1663
|
+
// A live checkout (`doc.previewVersion !== null`) starts / keeps the browsing
|
|
1326
1664
|
// session. Other read-only states (a view-only share link, a still-loading
|
|
1327
1665
|
// doc) are not this bar's concern, and `doc.id` is null only on the offline
|
|
1328
1666
|
// / test path the embed can't serve.
|
|
@@ -1331,8 +1669,8 @@ function buildVersionPreview(deps) {
|
|
|
1331
1669
|
// document is on and outranks the dismissal.
|
|
1332
1670
|
if (doc != null &&
|
|
1333
1671
|
docId !== null &&
|
|
1334
|
-
(doc.
|
|
1335
|
-
(doc.versionBrowsing || doc.
|
|
1672
|
+
(doc.previewVersion !== null || doc.versionBrowsing) &&
|
|
1673
|
+
(doc.versionBrowsing || doc.previewVersion !== dismissedAt)) {
|
|
1336
1674
|
browsing = true;
|
|
1337
1675
|
dismissedAt = undefined;
|
|
1338
1676
|
// Mirror the open bar onto the document, which is what makes it
|
|
@@ -1343,15 +1681,15 @@ function buildVersionPreview(deps) {
|
|
|
1343
1681
|
doc._setVersionBrowsing(true);
|
|
1344
1682
|
}
|
|
1345
1683
|
// The bar stays open for the whole session, including at the live head
|
|
1346
|
-
// (`doc.
|
|
1684
|
+
// (`doc.previewVersion === null`, browsing still true), until `close`.
|
|
1347
1685
|
if (doc != null && docId !== null && browsing) {
|
|
1348
1686
|
root.classList.add('b-version-on');
|
|
1349
1687
|
if (!frame)
|
|
1350
|
-
frame = createVersionBarFrame(root, frameDeps);
|
|
1688
|
+
frame = createVersionBarFrame(root, frameDeps());
|
|
1351
1689
|
frame.update({
|
|
1352
1690
|
docId,
|
|
1353
1691
|
branch: doc.branchId ?? undefined,
|
|
1354
|
-
version: doc.
|
|
1692
|
+
version: doc.previewVersion,
|
|
1355
1693
|
branchName: doc._currentBranchName() ?? undefined,
|
|
1356
1694
|
pinned: doc._isVersionPinned,
|
|
1357
1695
|
});
|
|
@@ -1393,7 +1731,7 @@ export function createDocumentTitle(container, deps) {
|
|
|
1393
1731
|
}
|
|
1394
1732
|
// The toolbar-surface commands (undo / redo) as prominent buttons.
|
|
1395
1733
|
export function createUndoRedo(container, deps) {
|
|
1396
|
-
return mountPiece(container, buildUndoRedo(deps));
|
|
1734
|
+
return mountPiece(container, buildUndoRedo(deps, 'standalone'));
|
|
1397
1735
|
}
|
|
1398
1736
|
// The presence roster (coloured initials of who's on the active doc).
|
|
1399
1737
|
// Receiver half only; the app still publishes its own state.
|
|
@@ -1430,9 +1768,13 @@ function buildBarShell() {
|
|
|
1430
1768
|
// row-gap, leaving asymmetric padding. "Empty" = every child is blank (no
|
|
1431
1769
|
// text, no element children), so an empty save-indicator span or a
|
|
1432
1770
|
// command-less undo/redo piece both count.
|
|
1771
|
+
// `b-app-tools` marks a strip holding something of the app's beside
|
|
1772
|
+
// undo / redo, which the narrow bar shows on a row of its own.
|
|
1433
1773
|
function updateToolsVisibility() {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1774
|
+
const blank = (c) => (c.textContent ?? '').trim() === '' && c.childElementCount === 0;
|
|
1775
|
+
const children = Array.from(tools.children);
|
|
1776
|
+
tools.style.display = children.every(blank) ? 'none' : '';
|
|
1777
|
+
bar.classList.toggle('b-app-tools', children.some((c) => !c.classList.contains('b-undo-full') && !blank(c)));
|
|
1436
1778
|
}
|
|
1437
1779
|
// The app (and the undo/redo subscription) mutates `tools` after mount, so
|
|
1438
1780
|
// re-evaluate visibility on any change.
|
|
@@ -1470,10 +1812,15 @@ export function createSessionToolbar(container, deps, options = {}) {
|
|
|
1470
1812
|
shell.start.append(drivePiece.root);
|
|
1471
1813
|
pieces.push(drivePiece);
|
|
1472
1814
|
}
|
|
1473
|
-
// Middle: SDK toolbar-surface commands (undo/redo) + the app's slot.
|
|
1815
|
+
// Middle: SDK toolbar-surface commands (undo/redo) + the app's slot. On a
|
|
1816
|
+
// narrow screen the middle wraps to a row of its own, so Undo and Redo show
|
|
1817
|
+
// there instead as icons in the right cluster (CSS swaps the two), and the
|
|
1818
|
+
// middle row only appears for the app's own tools.
|
|
1474
1819
|
const undoRedo = buildUndoRedo(deps);
|
|
1475
1820
|
shell.tools.append(undoRedo.root);
|
|
1476
|
-
|
|
1821
|
+
const compactUndo = buildUndoRedo(deps, 'compact');
|
|
1822
|
+
shell.end.append(compactUndo.root);
|
|
1823
|
+
pieces.push(undoRedo, compactUndo);
|
|
1477
1824
|
// Right cluster: presence roster + account menu.
|
|
1478
1825
|
if (showPresence) {
|
|
1479
1826
|
const rosterPiece = buildPresenceRoster(deps);
|
|
@@ -1485,12 +1832,14 @@ export function createSessionToolbar(container, deps, options = {}) {
|
|
|
1485
1832
|
shell.end.append(accountPiece.root);
|
|
1486
1833
|
pieces.push(accountPiece);
|
|
1487
1834
|
}
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1835
|
+
// Two full-width strips sit below the bar: the not-signed-in notice on a
|
|
1836
|
+
// document created without an account, and the historical-version preview
|
|
1837
|
+
// bar. Mounting the toolbar is all an app does to get both, so it never
|
|
1838
|
+
// hand-rolls either.
|
|
1839
|
+
const guestPiece = buildNotSignedIn(deps);
|
|
1491
1840
|
const versionPiece = buildVersionPreview(deps);
|
|
1492
|
-
pieces.push(versionPiece);
|
|
1493
|
-
container.replaceChildren(shell.bar, versionPiece.root);
|
|
1841
|
+
pieces.push(guestPiece, versionPiece);
|
|
1842
|
+
container.replaceChildren(shell.bar, guestPiece.root, versionPiece.root);
|
|
1494
1843
|
return {
|
|
1495
1844
|
tools: shell.tools,
|
|
1496
1845
|
destroy() {
|
|
@@ -1507,16 +1856,19 @@ export function createSessionToolbar(container, deps, options = {}) {
|
|
|
1507
1856
|
// never wraps when `tools` does.
|
|
1508
1857
|
export function createSessionHeader(container, deps) {
|
|
1509
1858
|
const shell = buildBarShell();
|
|
1510
|
-
// A header shell built with session access includes the
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1859
|
+
// A header shell built with session access includes the two strips below
|
|
1860
|
+
// the bar (the not-signed-in notice and the historical-version preview
|
|
1861
|
+
// bar), so a composable-chrome app gets what the full toolbar renders
|
|
1862
|
+
// there without a separate mount.
|
|
1863
|
+
const guestPiece = deps ? buildNotSignedIn(deps) : null;
|
|
1513
1864
|
const versionPiece = deps ? buildVersionPreview(deps) : null;
|
|
1514
|
-
container.replaceChildren(shell.bar, ...(versionPiece ? [versionPiece.root] : []));
|
|
1865
|
+
container.replaceChildren(shell.bar, ...(guestPiece ? [guestPiece.root] : []), ...(versionPiece ? [versionPiece.root] : []));
|
|
1515
1866
|
return {
|
|
1516
1867
|
start: shell.start,
|
|
1517
1868
|
tools: shell.tools,
|
|
1518
1869
|
end: shell.end,
|
|
1519
1870
|
destroy() {
|
|
1871
|
+
guestPiece?.destroy();
|
|
1520
1872
|
versionPiece?.destroy();
|
|
1521
1873
|
shell.destroy();
|
|
1522
1874
|
container.replaceChildren();
|