@backmatter/studio 0.0.14 → 0.0.16
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/dist/chunks/{UiKitApp-fYeVEf4-.js → UiKitApp-_EDqWID3.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-Dt7JvEWT.js → WorkspaceRoutes-B2PrFgpz.js} +29019 -27476
- package/dist/chunks/{dashboard-BuY2aGkO.js → dashboard-BnlnMnnr.js} +55 -54
- package/dist/chunks/{index-NWDqEZYW.js → index-D_BHF_8q.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +51 -52
- package/dist/site-widget.css +4 -0
- package/dist/site-widget.js +9419 -9156
- package/dist/studio.css +186 -12
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +64 -11
- package/src/chat/ChangeCard.tsx +9 -15
- package/src/chat/ChatMessage.tsx +172 -103
- package/src/chat/ChatPanel.tsx +54 -34
- package/src/chat/ChatProvider.tsx +120 -24
- package/src/chat/Composer.tsx +48 -7
- package/src/chat/DocListCard.tsx +2 -16
- package/src/chat/MentionPopover.tsx +8 -9
- package/src/chat/PointerChip.tsx +9 -5
- package/src/chat/ThreadHistory.tsx +3 -3
- package/src/chat/chat-context.ts +74 -10
- package/src/chat/chat-helpers.ts +15 -8
- package/src/chat/chat-state.ts +11 -0
- package/src/chrome/AppShell.tsx +16 -5
- package/src/chrome/CommandPalette.tsx +14 -10
- package/src/chrome/ContentExplorerSidebar.tsx +23 -3
- package/src/chrome/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +215 -50
- package/src/chrome/content-api-url.ts +0 -78
- package/src/chrome/content-tree-visibility.ts +13 -3
- package/src/chrome/explorer-icons.tsx +5 -0
- package/src/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -0
- package/src/host-pointer-bootstrap.ts +11 -0
- package/src/host-pointer.ts +147 -0
- package/src/install-preview-pointer.ts +19 -3
- package/src/pages/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1188 -0
- package/src/pages/DocEditorPage.tsx +33 -15
- package/src/pages/EmbedChatPage.tsx +3 -3
- package/src/pages/EmbedPublishPage.tsx +2 -3
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +51 -0
- package/src/pages/code-session-files.ts +42 -0
- package/src/pages/code-session-frame.ts +52 -0
- package/src/pages/code-session-routes.ts +42 -0
- package/src/pages/home-rows.ts +140 -0
- package/src/pages/preview-size.ts +33 -0
- package/src/pages/session-preview.ts +109 -0
- package/src/pages/type-docs.ts +3 -6
- package/src/pages/use-session-preview.ts +70 -0
- package/src/replica/store.ts +13 -10
- package/src/replica/types.ts +7 -2
- package/src/site-pointer.ts +24 -5
- package/src/site-widget.tsx +2 -3
- package/src/studio-config.ts +1 -1
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
|
@@ -7,9 +7,11 @@ import { Popover } from "@base-ui/react/popover";
|
|
|
7
7
|
import * as stylex from "@stylexjs/stylex";
|
|
8
8
|
import {
|
|
9
9
|
commitWorkspace,
|
|
10
|
+
commitCodeSessionEdits,
|
|
10
11
|
countConflicts,
|
|
11
12
|
createGitBranch,
|
|
12
13
|
defaultCommitMessage,
|
|
14
|
+
fetchCodeSessionEdits,
|
|
13
15
|
fetchGit,
|
|
14
16
|
notifyHeadChanged,
|
|
15
17
|
notifyTreeChanged,
|
|
@@ -17,10 +19,12 @@ import {
|
|
|
17
19
|
notifyWorkspaceChanged,
|
|
18
20
|
pullFromGithub,
|
|
19
21
|
resolveDoc,
|
|
22
|
+
type ApiError,
|
|
20
23
|
type ChangeStatus,
|
|
21
24
|
type GitChange,
|
|
22
25
|
} from "../api";
|
|
23
|
-
import { contentDocPath } from "../workspace-path";
|
|
26
|
+
import { contentDocPath, isWorkspaceCodePath } from "../workspace-path";
|
|
27
|
+
import { useCodeSessionContext } from "../pages/code-session-context";
|
|
24
28
|
import {
|
|
25
29
|
commitErrorSummary,
|
|
26
30
|
explainCommitError,
|
|
@@ -33,8 +37,8 @@ import { WorkspaceSwitcher } from "../components/WorkspaceSwitcher";
|
|
|
33
37
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
34
38
|
import { cx, popup, popupClassName } from "../ui/popup";
|
|
35
39
|
import { useEditorStatus } from "./EditorStatusContext";
|
|
36
|
-
import {
|
|
37
|
-
import { OpenSiteButton } from "./OpenSiteButton";
|
|
40
|
+
import { openableSites } from "./open-site";
|
|
41
|
+
import { OpenSiteButton, showsOpenSite } from "./OpenSiteButton";
|
|
38
42
|
import { GitBranchIcon } from "./icons";
|
|
39
43
|
import { StatusBar, StatusItem, StatusSpacer } from "./StatusBar";
|
|
40
44
|
import { replicaStore } from "../replica/instance";
|
|
@@ -56,12 +60,11 @@ export const STATUS_LABEL: Record<ChangeStatus, string> = {
|
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
function changeKey(doc: GitChange) {
|
|
59
|
-
return `${doc.
|
|
63
|
+
return `${doc.type}/${doc.slug}`;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function ConflictResolvePanel({
|
|
63
67
|
workspaceSlug,
|
|
64
|
-
defaultLocale,
|
|
65
68
|
conflicts,
|
|
66
69
|
resolvingKey,
|
|
67
70
|
error,
|
|
@@ -69,7 +72,6 @@ function ConflictResolvePanel({
|
|
|
69
72
|
onResolve,
|
|
70
73
|
}: {
|
|
71
74
|
workspaceSlug: string | null;
|
|
72
|
-
defaultLocale: string;
|
|
73
75
|
conflicts: GitChange[];
|
|
74
76
|
resolvingKey: string | null;
|
|
75
77
|
error: string | null;
|
|
@@ -88,13 +90,7 @@ function ConflictResolvePanel({
|
|
|
88
90
|
{conflicts.map((doc) => {
|
|
89
91
|
const key = changeKey(doc);
|
|
90
92
|
const href = workspaceSlug
|
|
91
|
-
? contentDocPath(
|
|
92
|
-
workspaceSlug,
|
|
93
|
-
doc.type,
|
|
94
|
-
doc.slug,
|
|
95
|
-
doc.locale,
|
|
96
|
-
defaultLocale,
|
|
97
|
-
)
|
|
93
|
+
? contentDocPath(workspaceSlug, doc.type, doc.slug)
|
|
98
94
|
: null;
|
|
99
95
|
return (
|
|
100
96
|
<li key={key} {...stylex.props(styles.conflictRow)}>
|
|
@@ -107,7 +103,6 @@ function ConflictResolvePanel({
|
|
|
107
103
|
<span {...stylex.props(styles.changeName)}>
|
|
108
104
|
{doc.type}/{doc.slug}
|
|
109
105
|
</span>
|
|
110
|
-
<span {...stylex.props(popup.itemMeta)}>{doc.locale}</span>
|
|
111
106
|
</Link>
|
|
112
107
|
) : (
|
|
113
108
|
<span {...stylex.props(styles.changeName)}>
|
|
@@ -178,6 +173,15 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
178
173
|
const [message, setMessage] = useState("");
|
|
179
174
|
const [error, setError] = useState<string | null>(null);
|
|
180
175
|
const [commitFailure, setCommitFailure] = useState<CommitErrorView | null>(null);
|
|
176
|
+
const onCodePage = isWorkspaceCodePath(location.pathname);
|
|
177
|
+
const codeSession = useCodeSessionContext();
|
|
178
|
+
const codeWorkspaceId = workspaceNav?.workspaceId ?? workspaceId;
|
|
179
|
+
const [codeFiles, setCodeFiles] = useState<Array<{ path: string; deleted: boolean }>>([]);
|
|
180
|
+
const [foldPending, setFoldPending] = useState(false);
|
|
181
|
+
const [codeFoldErrors, setCodeFoldErrors] = useState<string[]>([]);
|
|
182
|
+
const [codeCommitOpen, setCodeCommitOpen] = useState(false);
|
|
183
|
+
const [codeMessage, setCodeMessage] = useState("");
|
|
184
|
+
const [codeCommitting, setCodeCommitting] = useState(false);
|
|
181
185
|
// Commit sha from the last poll. A new sha means HEAD moved. The preview
|
|
182
186
|
// iframe does not reload for that; its page follows the SSE head event.
|
|
183
187
|
const lastHead = useRef<string | null | undefined>(undefined);
|
|
@@ -201,6 +205,35 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!onCodePage || !codeSession) {
|
|
210
|
+
setCodeFiles([]);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
const load = () => {
|
|
215
|
+
void fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId)
|
|
216
|
+
.then((list) => {
|
|
217
|
+
if (!cancelled) {
|
|
218
|
+
setCodeFiles(list.files);
|
|
219
|
+
setFoldPending(list.foldPending);
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
.catch(() => {
|
|
223
|
+
if (!cancelled) {
|
|
224
|
+
setCodeFiles([]);
|
|
225
|
+
setFoldPending(false);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
load();
|
|
230
|
+
const timer = window.setInterval(load, POLL_MS);
|
|
231
|
+
return () => {
|
|
232
|
+
cancelled = true;
|
|
233
|
+
window.clearInterval(timer);
|
|
234
|
+
};
|
|
235
|
+
}, [onCodePage, codeSession, codeWorkspaceId]);
|
|
236
|
+
|
|
204
237
|
useEffect(() => {
|
|
205
238
|
lastHead.current = undefined;
|
|
206
239
|
void reloadGit();
|
|
@@ -293,10 +326,9 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
293
326
|
setResolvingKey(key);
|
|
294
327
|
setError(null);
|
|
295
328
|
try {
|
|
296
|
-
await resolveDoc(workspaceId, doc.
|
|
329
|
+
await resolveDoc(workspaceId, doc.type, doc.slug, strategy);
|
|
297
330
|
notifyTreeChanged({
|
|
298
331
|
workspaceId,
|
|
299
|
-
locale: doc.locale,
|
|
300
332
|
type: doc.type,
|
|
301
333
|
slug: doc.slug,
|
|
302
334
|
status: strategy === "theirs" ? "clean" : "modified",
|
|
@@ -314,6 +346,46 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
314
346
|
}
|
|
315
347
|
}
|
|
316
348
|
|
|
349
|
+
function defaultCodeSessionMessage(files: Array<{ path: string }>) {
|
|
350
|
+
if (files.length === 1) return `Update ${files[0]!.path}`;
|
|
351
|
+
return `Commit ${files.length} files from a code session`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function onCodeSessionCommit(e?: React.FormEvent) {
|
|
355
|
+
e?.preventDefault();
|
|
356
|
+
if (!codeSession || codeCommitting) return;
|
|
357
|
+
setCodeCommitting(true);
|
|
358
|
+
setError(null);
|
|
359
|
+
try {
|
|
360
|
+
const result = await commitCodeSessionEdits(
|
|
361
|
+
codeWorkspaceId,
|
|
362
|
+
codeSession.sessionId,
|
|
363
|
+
codeMessage,
|
|
364
|
+
);
|
|
365
|
+
setCodeMessage("");
|
|
366
|
+
setCodeFoldErrors(result.foldErrors);
|
|
367
|
+
if (result.foldErrors.length === 0) setCodeCommitOpen(false);
|
|
368
|
+
codeSession.refresh();
|
|
369
|
+
const list = await fetchCodeSessionEdits(codeWorkspaceId, codeSession.sessionId);
|
|
370
|
+
setCodeFiles(list.files);
|
|
371
|
+
setFoldPending(list.foldPending);
|
|
372
|
+
notifyHeadChanged();
|
|
373
|
+
await reloadGit();
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const apiErr = err as ApiError;
|
|
376
|
+
if (apiErr.code === "branch_moved") {
|
|
377
|
+
setError("The branch changed while committing. Try again.");
|
|
378
|
+
} else if (apiErr.code === "working_tree") {
|
|
379
|
+
const paths = apiErr.details?.length ? apiErr.details.join(", ") : "";
|
|
380
|
+
setError(paths ? `Uncommitted Studio changes: ${paths}` : "Uncommitted Studio changes block this commit.");
|
|
381
|
+
} else {
|
|
382
|
+
setError(err instanceof Error ? err.message : "Commit failed");
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
setCodeCommitting(false);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
317
389
|
async function onCommit(e?: React.FormEvent) {
|
|
318
390
|
e?.preventDefault();
|
|
319
391
|
if (committing || conflictCount > 0 || changeCount === 0) return;
|
|
@@ -341,20 +413,27 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
341
413
|
const slug = ref.slice(slash + 1);
|
|
342
414
|
const doc = changes.find((change) => change.type === type && change.slug === slug);
|
|
343
415
|
if (!doc) return null;
|
|
344
|
-
return contentDocPath(
|
|
345
|
-
workspaceNav.workspaceSlug,
|
|
346
|
-
doc.type,
|
|
347
|
-
doc.slug,
|
|
348
|
-
doc.locale,
|
|
349
|
-
workspaceNav.defaultLocale,
|
|
350
|
-
);
|
|
416
|
+
return contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug);
|
|
351
417
|
}
|
|
352
418
|
|
|
353
419
|
const commitLabel = committing
|
|
354
420
|
? "Committing…"
|
|
355
421
|
: conflictCount > 0
|
|
356
422
|
? `Resolve ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}`
|
|
357
|
-
:
|
|
423
|
+
: changeCount === 0
|
|
424
|
+
? "Deploy"
|
|
425
|
+
: `Deploy ${changeCount} changes`;
|
|
426
|
+
|
|
427
|
+
const codeEditCount =
|
|
428
|
+
codeFiles.length > 0 ? codeFiles.length : (codeSession?.editCount ?? 0);
|
|
429
|
+
const codeCommitReady =
|
|
430
|
+
Boolean(codeSession) &&
|
|
431
|
+
(codeEditCount > 0 || codeFoldErrors.length > 0 || foldPending);
|
|
432
|
+
const codeCommitLabel = codeCommitting
|
|
433
|
+
? "Committing…"
|
|
434
|
+
: codeEditCount > 0
|
|
435
|
+
? `Commit ${codeEditCount} ${codeEditCount === 1 ? "file" : "files"}`
|
|
436
|
+
: "Commit";
|
|
358
437
|
|
|
359
438
|
return (
|
|
360
439
|
<StatusBar>
|
|
@@ -444,7 +523,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
444
523
|
<div {...stylex.props(styles.changes)}>
|
|
445
524
|
<p {...stylex.props(popup.groupLabel)}>Uncommitted</p>
|
|
446
525
|
{changes.map((doc) => (
|
|
447
|
-
<ChangeRow key={
|
|
526
|
+
<ChangeRow key={changeKey(doc)} doc={doc} />
|
|
448
527
|
))}
|
|
449
528
|
</div>
|
|
450
529
|
</>
|
|
@@ -494,27 +573,21 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
494
573
|
</Combobox.Positioner>
|
|
495
574
|
</Combobox.Portal>
|
|
496
575
|
</Combobox.Root>
|
|
497
|
-
{workspaceNav?.workspaceId
|
|
576
|
+
{workspaceNav?.workspaceId &&
|
|
577
|
+
replica.sessionUserId &&
|
|
578
|
+
showsOpenSite() &&
|
|
579
|
+
openableSites(workspaceNav.siteUrls).length > 0 ? (
|
|
498
580
|
<>
|
|
499
581
|
<span {...stylex.props(styles.divider)} aria-hidden />
|
|
500
|
-
<
|
|
582
|
+
<OpenSiteButton
|
|
501
583
|
workspaceId={workspaceNav.workspaceId}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
pathname={location.pathname}
|
|
505
|
-
search={location.search}
|
|
584
|
+
userId={replica.sessionUserId}
|
|
585
|
+
siteUrls={workspaceNav.siteUrls}
|
|
506
586
|
/>
|
|
507
|
-
{replica.sessionUserId ? (
|
|
508
|
-
<OpenSiteButton
|
|
509
|
-
workspaceId={workspaceNav.workspaceId}
|
|
510
|
-
userId={replica.sessionUserId}
|
|
511
|
-
siteUrls={workspaceNav.siteUrls}
|
|
512
|
-
/>
|
|
513
|
-
) : null}
|
|
514
587
|
</>
|
|
515
588
|
) : null}
|
|
516
589
|
</div>
|
|
517
|
-
{changeCount > 0 ? (
|
|
590
|
+
{!onCodePage && changeCount > 0 ? (
|
|
518
591
|
<StatusItem tone={conflictCount > 0 ? "danger" : "muted"}>
|
|
519
592
|
{changeCount} uncommitted
|
|
520
593
|
</StatusItem>
|
|
@@ -530,24 +603,117 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
530
603
|
{inEditor && editor.saveLabel ? (
|
|
531
604
|
<StatusItem tone={editor.saveTone}>{editor.saveLabel}</StatusItem>
|
|
532
605
|
) : null}
|
|
533
|
-
{
|
|
606
|
+
{onCodePage ? (
|
|
607
|
+
<Popover.Root
|
|
608
|
+
open={codeCommitOpen}
|
|
609
|
+
onOpenChange={(next) => {
|
|
610
|
+
if (!codeCommitReady) return;
|
|
611
|
+
setCodeCommitOpen(next);
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<Popover.Trigger
|
|
615
|
+
disabled={!codeCommitReady || codeCommitting}
|
|
616
|
+
aria-label={codeCommitLabel}
|
|
617
|
+
className={cx(styles.commit)}
|
|
618
|
+
>
|
|
619
|
+
{codeCommitLabel}
|
|
620
|
+
</Popover.Trigger>
|
|
621
|
+
<Popover.Portal>
|
|
622
|
+
<Popover.Positioner
|
|
623
|
+
side="bottom"
|
|
624
|
+
align="end"
|
|
625
|
+
sideOffset={6}
|
|
626
|
+
className={cx(popup.positioner)}
|
|
627
|
+
>
|
|
628
|
+
<Popover.Popup
|
|
629
|
+
aria-label="Commit code session"
|
|
630
|
+
className={(state) => popupClassName(state, styles.commitPopup)}
|
|
631
|
+
>
|
|
632
|
+
<form
|
|
633
|
+
onSubmit={(e) => void onCodeSessionCommit(e)}
|
|
634
|
+
{...stylex.props(styles.commitForm)}
|
|
635
|
+
>
|
|
636
|
+
<p {...stylex.props(popup.groupLabel)}>
|
|
637
|
+
{codeEditCount} {codeEditCount === 1 ? "file" : "files"} →{" "}
|
|
638
|
+
{git?.currentBranch}
|
|
639
|
+
</p>
|
|
640
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
641
|
+
{codeFiles.map((file) => (
|
|
642
|
+
<li key={file.path}>
|
|
643
|
+
<div {...stylex.props(styles.changeItem)}>
|
|
644
|
+
<span {...stylex.props(styles.changeName)}>{file.path}</span>
|
|
645
|
+
<span {...stylex.props(popup.itemMeta)}>
|
|
646
|
+
{file.deleted ? "deleted" : "modified"}
|
|
647
|
+
</span>
|
|
648
|
+
</div>
|
|
649
|
+
</li>
|
|
650
|
+
))}
|
|
651
|
+
</ul>
|
|
652
|
+
{codeFoldErrors.length > 0 ? (
|
|
653
|
+
<ul {...stylex.props(styles.commitFiles)}>
|
|
654
|
+
{codeFoldErrors.map((foldError) => (
|
|
655
|
+
<li key={foldError}>
|
|
656
|
+
<p {...stylex.props(styles.conflictError)}>{foldError}</p>
|
|
657
|
+
</li>
|
|
658
|
+
))}
|
|
659
|
+
</ul>
|
|
660
|
+
) : null}
|
|
661
|
+
<div {...stylex.props(popup.separator)} />
|
|
662
|
+
<div {...stylex.props(styles.commitFooter)}>
|
|
663
|
+
<input
|
|
664
|
+
value={codeMessage}
|
|
665
|
+
onChange={(e) => setCodeMessage(e.target.value)}
|
|
666
|
+
placeholder={defaultCodeSessionMessage(codeFiles)}
|
|
667
|
+
aria-label="Commit message"
|
|
668
|
+
autoComplete="off"
|
|
669
|
+
spellCheck={false}
|
|
670
|
+
data-no-ring
|
|
671
|
+
data-1p-ignore
|
|
672
|
+
data-lpignore
|
|
673
|
+
autoFocus
|
|
674
|
+
disabled={codeCommitting}
|
|
675
|
+
{...stylex.props(styles.commitField)}
|
|
676
|
+
/>
|
|
677
|
+
<div {...stylex.props(styles.commitSubmit)}>
|
|
678
|
+
<button
|
|
679
|
+
type="submit"
|
|
680
|
+
disabled={codeCommitting}
|
|
681
|
+
{...stylex.props(styles.commitSubmitBtn)}
|
|
682
|
+
>
|
|
683
|
+
{codeCommitting ? "Committing…" : "Commit"}
|
|
684
|
+
</button>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</form>
|
|
688
|
+
</Popover.Popup>
|
|
689
|
+
</Popover.Positioner>
|
|
690
|
+
</Popover.Portal>
|
|
691
|
+
</Popover.Root>
|
|
692
|
+
) : null}
|
|
693
|
+
{!onCodePage ? (
|
|
534
694
|
<Popover.Root
|
|
535
695
|
open={commitOpen}
|
|
536
696
|
onOpenChange={(next) => {
|
|
537
|
-
if (action === "blocked") return;
|
|
697
|
+
if (changeCount === 0 || action === "blocked") return;
|
|
538
698
|
setCommitOpen(next);
|
|
539
699
|
}}
|
|
540
700
|
>
|
|
541
701
|
<Popover.Trigger
|
|
542
|
-
disabled={
|
|
702
|
+
disabled={
|
|
703
|
+
changeCount === 0 || commitTriggerDisabled(action, { committing, syncing })
|
|
704
|
+
}
|
|
543
705
|
aria-label={
|
|
544
|
-
|
|
545
|
-
? "
|
|
546
|
-
: action === "
|
|
547
|
-
?
|
|
548
|
-
:
|
|
706
|
+
changeCount === 0
|
|
707
|
+
? "Deploy"
|
|
708
|
+
: action === "blocked"
|
|
709
|
+
? "Writers cannot publish"
|
|
710
|
+
: action === "resolve"
|
|
711
|
+
? commitLabel
|
|
712
|
+
: `Commit ${changeCount} ${changeCount === 1 ? "file" : "files"}`
|
|
713
|
+
}
|
|
714
|
+
title={
|
|
715
|
+
changeCount > 0 && action === "blocked" ? "Writers cannot publish" : undefined
|
|
549
716
|
}
|
|
550
|
-
title={action === "blocked" ? "Writers cannot publish" : undefined}
|
|
551
717
|
className={cx(styles.commit, action === "resolve" && styles.commitBlocked)}
|
|
552
718
|
>
|
|
553
719
|
{commitLabel}
|
|
@@ -566,7 +732,6 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
566
732
|
{action === "resolve" ? (
|
|
567
733
|
<ConflictResolvePanel
|
|
568
734
|
workspaceSlug={workspaceNav?.workspaceSlug ?? null}
|
|
569
|
-
defaultLocale={workspaceNav?.defaultLocale ?? "en"}
|
|
570
735
|
conflicts={conflicts}
|
|
571
736
|
resolvingKey={resolvingKey}
|
|
572
737
|
error={error}
|
|
@@ -598,7 +763,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
|
|
|
598
763
|
) : null}
|
|
599
764
|
<ul {...stylex.props(styles.commitFiles)}>
|
|
600
765
|
{changes.map((doc) => (
|
|
601
|
-
<li key={
|
|
766
|
+
<li key={changeKey(doc)}>
|
|
602
767
|
<ChangeRow
|
|
603
768
|
doc={doc}
|
|
604
769
|
invalid={
|
|
@@ -1,48 +1,6 @@
|
|
|
1
1
|
// packages/studio/src/chrome/content-api-url.ts
|
|
2
2
|
// Public content GET the host reads — same URLs as @backmatter/core api-loader.
|
|
3
3
|
|
|
4
|
-
export type ContentApiDoc = { type: string; slug: string };
|
|
5
|
-
|
|
6
|
-
export type ContentApiContext = {
|
|
7
|
-
doc: ContentApiDoc | null;
|
|
8
|
-
type: string | null;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function contentApiContextFromPath(
|
|
12
|
-
pathname: string,
|
|
13
|
-
_search = "",
|
|
14
|
-
): ContentApiContext {
|
|
15
|
-
const doc = pathname.match(/\/content\/([^/]+)\/([^/]+)\/?$/);
|
|
16
|
-
if (doc) {
|
|
17
|
-
return {
|
|
18
|
-
doc: {
|
|
19
|
-
type: decodeURIComponent(doc[1]!),
|
|
20
|
-
slug: decodeURIComponent(doc[2]!),
|
|
21
|
-
},
|
|
22
|
-
type: null,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const type = pathname.match(/\/content\/([^/]+)\/?$/);
|
|
26
|
-
if (type) {
|
|
27
|
-
return { doc: null, type: decodeURIComponent(type[1]!) };
|
|
28
|
-
}
|
|
29
|
-
const legacyDoc = pathname.match(/\/docs\/([^/]+)\/([^/]+)\/([^/]+)\/?$/);
|
|
30
|
-
if (legacyDoc) {
|
|
31
|
-
return {
|
|
32
|
-
doc: {
|
|
33
|
-
type: decodeURIComponent(legacyDoc[2]!),
|
|
34
|
-
slug: decodeURIComponent(legacyDoc[3]!),
|
|
35
|
-
},
|
|
36
|
-
type: null,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const legacyType = pathname.match(/\/types\/([^/]+)\/?$/);
|
|
40
|
-
if (legacyType) {
|
|
41
|
-
return { doc: null, type: decodeURIComponent(legacyType[1]!) };
|
|
42
|
-
}
|
|
43
|
-
return { doc: null, type: null };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
4
|
export function publicContentApiUrl(input: {
|
|
47
5
|
apiUrl: string;
|
|
48
6
|
workspaceId: string;
|
|
@@ -64,39 +22,3 @@ export function publicContentApiUrl(input: {
|
|
|
64
22
|
if (!input.slug && input.type) url.searchParams.set("type", input.type);
|
|
65
23
|
return url.toString();
|
|
66
24
|
}
|
|
67
|
-
|
|
68
|
-
export function contentApiLinks(input: {
|
|
69
|
-
apiUrl: string;
|
|
70
|
-
workspaceId: string;
|
|
71
|
-
branch?: string | null;
|
|
72
|
-
defaultBranch?: string | null;
|
|
73
|
-
pathname: string;
|
|
74
|
-
search?: string;
|
|
75
|
-
}): Array<{ label: string; href: string }> {
|
|
76
|
-
const ctx = contentApiContextFromPath(input.pathname, input.search);
|
|
77
|
-
const base = {
|
|
78
|
-
apiUrl: input.apiUrl,
|
|
79
|
-
workspaceId: input.workspaceId,
|
|
80
|
-
branch: input.branch,
|
|
81
|
-
defaultBranch: input.defaultBranch,
|
|
82
|
-
};
|
|
83
|
-
const links: Array<{ label: string; href: string }> = [];
|
|
84
|
-
if (ctx.doc) {
|
|
85
|
-
links.push({
|
|
86
|
-
label: "This document",
|
|
87
|
-
href: publicContentApiUrl({
|
|
88
|
-
...base,
|
|
89
|
-
type: ctx.doc.type,
|
|
90
|
-
slug: ctx.doc.slug,
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
links.push({
|
|
95
|
-
label: ctx.type ? `${ctx.type} docs` : "All docs",
|
|
96
|
-
href: publicContentApiUrl({
|
|
97
|
-
...base,
|
|
98
|
-
type: ctx.type ?? undefined,
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
return links;
|
|
102
|
-
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// packages/studio/src/chrome/content-tree-visibility.ts
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import type { KeyValueStore } from "../last-workspace";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isWorkspaceContentPath,
|
|
6
|
+
isWorkspaceEditorPath,
|
|
7
|
+
isWorkspaceHomePath,
|
|
8
|
+
} from "../workspace-path";
|
|
5
9
|
|
|
6
10
|
export const CONTENT_TREE_OPEN_KEY = "backmatter-content-tree-open";
|
|
7
11
|
const CONTENT_TREE_EVENT = "backmatter-content-tree";
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
* The type index
|
|
11
|
-
* A document follows the editor toggle.
|
|
14
|
+
* The content index and a type index keep the file tree open.
|
|
15
|
+
* A document follows the editor toggle. Home, settings, and media have no tree.
|
|
12
16
|
*/
|
|
13
17
|
export function contentTreeVisible(pathname: string, open: boolean) {
|
|
18
|
+
if (isWorkspaceHomePath(pathname)) return false;
|
|
14
19
|
if (!isWorkspaceContentPath(pathname)) return false;
|
|
15
20
|
if (isWorkspaceEditorPath(pathname)) return open;
|
|
16
21
|
return true;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/** Content rail is current wherever the file tree is open. */
|
|
25
|
+
export function contentRailCurrent(pathname: string, treeOpen: boolean) {
|
|
26
|
+
return contentTreeVisible(pathname, treeOpen);
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function readContentTreeOpen(storage: KeyValueStore = localStorage) {
|
|
20
30
|
try {
|
|
21
31
|
return storage.getItem(CONTENT_TREE_OPEN_KEY) !== "0";
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
IconChevronRightSmall,
|
|
8
8
|
IconBrackets2,
|
|
9
9
|
IconFileText,
|
|
10
|
+
IconCode,
|
|
10
11
|
IconFiles,
|
|
11
12
|
IconFolder1,
|
|
12
13
|
IconGit,
|
|
@@ -70,6 +71,10 @@ export function ExplorerRailMediaIcon() {
|
|
|
70
71
|
return <Central icon={IconImages1} size={RAIL} />;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
export function ExplorerRailCodeIcon() {
|
|
75
|
+
return <Central icon={IconCode} size={RAIL} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
export function ExplorerRailSettingsIcon() {
|
|
74
79
|
return <Central icon={IconSettingsGear2} size={RAIL} />;
|
|
75
80
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// packages/studio/src/code-session-entry.ts
|
|
2
|
+
import { canDevelop } from "./workspace/permissions";
|
|
3
|
+
import { codeSessionPath } from "./workspace-path";
|
|
4
|
+
|
|
5
|
+
/** Where a developer opens a code session. Writers and editors get nothing. */
|
|
6
|
+
export function codeSessionEntry(role: string, slug: string): string | null {
|
|
7
|
+
if (!canDevelop(role)) return null;
|
|
8
|
+
return codeSessionPath(slug);
|
|
9
|
+
}
|
|
@@ -47,9 +47,9 @@ export function WorkspaceSwitcher({
|
|
|
47
47
|
(id: string) => {
|
|
48
48
|
setOpen(false);
|
|
49
49
|
const next = workspaces.find((workspace) => workspace.id === id);
|
|
50
|
-
if (next
|
|
50
|
+
if (next) navigate(workspaceHomePath(next.slug));
|
|
51
51
|
},
|
|
52
|
-
[navigate,
|
|
52
|
+
[navigate, workspaces],
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
// Number keys 1–9 jump straight to a workspace while the menu is open.
|