@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
package/src/chat/chat-context.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// packages/studio/src/chat/chat-context.ts
|
|
2
2
|
import {
|
|
3
|
+
docKey,
|
|
3
4
|
sameDocRef,
|
|
4
5
|
type DocRef,
|
|
5
6
|
type EditorFocus,
|
|
@@ -7,13 +8,20 @@ import {
|
|
|
7
8
|
|
|
8
9
|
/** An element the user pointed at on the site. The agent edits that block, or the quote. */
|
|
9
10
|
export type ChatPointer = {
|
|
10
|
-
locale: string;
|
|
11
11
|
type: string;
|
|
12
12
|
slug: string;
|
|
13
13
|
blockId: string | null;
|
|
14
14
|
blockType: string | null;
|
|
15
15
|
blockIndex: number | null;
|
|
16
16
|
quote: string;
|
|
17
|
+
/** React component that rendered the element, when the dev server exposes it. */
|
|
18
|
+
component?: string | null;
|
|
19
|
+
/** Checkout-relative source path. */
|
|
20
|
+
file?: string | null;
|
|
21
|
+
line?: number | null;
|
|
22
|
+
column?: number | null;
|
|
23
|
+
/** Component names from the element toward the layout. */
|
|
24
|
+
stack?: string[];
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
export const POINTER_CAP = 8;
|
|
@@ -24,15 +32,23 @@ export type ChatRequestContext = {
|
|
|
24
32
|
selection: { start: number; end: number } | null;
|
|
25
33
|
mentions: DocRef[];
|
|
26
34
|
pointers: ChatPointer[];
|
|
35
|
+
route: string | null;
|
|
27
36
|
};
|
|
28
37
|
|
|
38
|
+
function parseChatRoute(raw: unknown): string | null {
|
|
39
|
+
if (typeof raw !== "string") return null;
|
|
40
|
+
return raw.startsWith("/") && !raw.startsWith("//") ? raw : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
function samePointer(a: ChatPointer, b: ChatPointer): boolean {
|
|
30
44
|
return (
|
|
31
|
-
a.locale === b.locale &&
|
|
32
45
|
a.type === b.type &&
|
|
33
46
|
a.slug === b.slug &&
|
|
34
47
|
a.blockId === b.blockId &&
|
|
35
|
-
a.quote === b.quote
|
|
48
|
+
a.quote === b.quote &&
|
|
49
|
+
(a.component ?? null) === (b.component ?? null) &&
|
|
50
|
+
(a.file ?? null) === (b.file ?? null) &&
|
|
51
|
+
(a.line ?? null) === (b.line ?? null)
|
|
36
52
|
);
|
|
37
53
|
}
|
|
38
54
|
|
|
@@ -50,7 +66,12 @@ export function pointersFromMetadata(metadata: unknown): ChatPointer[] {
|
|
|
50
66
|
return pointers.slice(0, POINTER_CAP);
|
|
51
67
|
}
|
|
52
68
|
|
|
53
|
-
export function addPointer(
|
|
69
|
+
export function addPointer(
|
|
70
|
+
list: ChatPointer[],
|
|
71
|
+
next: ChatPointer,
|
|
72
|
+
mode: "append" | "replace" = "append",
|
|
73
|
+
): ChatPointer[] {
|
|
74
|
+
if (mode === "replace") return [next];
|
|
54
75
|
if (list.some((item) => samePointer(item, next))) return list;
|
|
55
76
|
const added = [...list, next];
|
|
56
77
|
return added.length > POINTER_CAP ? added.slice(added.length - POINTER_CAP) : added;
|
|
@@ -61,13 +82,13 @@ export function removePointer(list: ChatPointer[], next: ChatPointer): ChatPoint
|
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export function chatPointerFromGrab(
|
|
64
|
-
doc: {
|
|
85
|
+
doc: { type?: unknown; slug?: unknown },
|
|
65
86
|
grab: unknown,
|
|
66
87
|
): ChatPointer | null {
|
|
67
|
-
if (!doc || typeof doc.
|
|
88
|
+
if (!doc || typeof doc.type !== "string" || typeof doc.slug !== "string") {
|
|
68
89
|
return null;
|
|
69
90
|
}
|
|
70
|
-
if (!doc.
|
|
91
|
+
if (!doc.type || !doc.slug) return null;
|
|
71
92
|
if (!grab || typeof grab !== "object") return null;
|
|
72
93
|
const value = grab as Record<string, unknown>;
|
|
73
94
|
if (value.blockId !== null && typeof value.blockId !== "string") return null;
|
|
@@ -75,21 +96,56 @@ export function chatPointerFromGrab(
|
|
|
75
96
|
if (value.blockIndex !== null && typeof value.blockIndex !== "number") return null;
|
|
76
97
|
if (typeof value.quote !== "string") return null;
|
|
77
98
|
return {
|
|
78
|
-
locale: doc.locale,
|
|
79
99
|
type: doc.type,
|
|
80
100
|
slug: doc.slug,
|
|
81
101
|
blockId: value.blockId,
|
|
82
102
|
blockType: value.blockType,
|
|
83
103
|
blockIndex: value.blockIndex,
|
|
84
104
|
quote: value.quote.replace(/\s+/g, " ").trim().slice(0, 240),
|
|
105
|
+
...placeFrom(value),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function placeFrom(value: Record<string, unknown>): Pick<ChatPointer, "component" | "file" | "line" | "column" | "stack"> {
|
|
110
|
+
const component = typeof value.component === "string" && value.component ? value.component.slice(0, 80) : null;
|
|
111
|
+
const file =
|
|
112
|
+
typeof value.file === "string" && /^[\w./@+-]+\.(?:tsx|ts|jsx|js|mdx)$/.test(value.file) && !value.file.includes("..")
|
|
113
|
+
? value.file.slice(0, 240)
|
|
114
|
+
: null;
|
|
115
|
+
const line = typeof value.line === "number" && Number.isInteger(value.line) && value.line > 0 ? value.line : null;
|
|
116
|
+
const column = typeof value.column === "number" && Number.isInteger(value.column) && value.column > 0 ? value.column : null;
|
|
117
|
+
const stack = Array.isArray(value.stack)
|
|
118
|
+
? value.stack.filter((item): item is string => typeof item === "string" && item.length > 0).slice(0, 4)
|
|
119
|
+
: [];
|
|
120
|
+
return {
|
|
121
|
+
...(component ? { component } : {}),
|
|
122
|
+
...(file ? { file } : {}),
|
|
123
|
+
...(line ? { line } : {}),
|
|
124
|
+
...(column ? { column } : {}),
|
|
125
|
+
...(stack.length > 0 ? { stack } : {}),
|
|
85
126
|
};
|
|
86
127
|
}
|
|
87
128
|
|
|
129
|
+
/** A code session sends the route and the pointed blocks, not the open document. */
|
|
130
|
+
export function codeSessionChatContext(input: {
|
|
131
|
+
pointers: ChatPointer[];
|
|
132
|
+
route?: string | null;
|
|
133
|
+
}): ChatRequestContext {
|
|
134
|
+
return buildChatContext({
|
|
135
|
+
focus: null,
|
|
136
|
+
includeFocus: false,
|
|
137
|
+
mentions: [],
|
|
138
|
+
pointers: input.pointers,
|
|
139
|
+
route: input.route,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
88
143
|
export function buildChatContext(input: {
|
|
89
144
|
focus: EditorFocus | null;
|
|
90
145
|
includeFocus: boolean;
|
|
91
146
|
mentions: DocRef[];
|
|
92
147
|
pointers: ChatPointer[];
|
|
148
|
+
route?: string | null;
|
|
93
149
|
}): ChatRequestContext {
|
|
94
150
|
const focusDoc = input.includeFocus && input.focus ? input.focus.docRef : null;
|
|
95
151
|
const selection =
|
|
@@ -100,14 +156,20 @@ export function buildChatContext(input: {
|
|
|
100
156
|
for (const m of input.mentions) {
|
|
101
157
|
if (sameDocRef(m, focusDoc)) continue;
|
|
102
158
|
if (mentions.some((x) => sameDocRef(x, m))) continue;
|
|
103
|
-
mentions.push({
|
|
159
|
+
mentions.push({ type: m.type, slug: m.slug });
|
|
104
160
|
}
|
|
105
161
|
const pointers: ChatPointer[] = [];
|
|
106
162
|
for (const pointer of input.pointers) {
|
|
107
163
|
if (pointers.some((item) => samePointer(item, pointer))) continue;
|
|
108
164
|
pointers.push(pointer);
|
|
109
165
|
}
|
|
110
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
focusDoc,
|
|
168
|
+
selection,
|
|
169
|
+
mentions,
|
|
170
|
+
pointers: pointers.slice(-POINTER_CAP),
|
|
171
|
+
route: parseChatRoute(input.route),
|
|
172
|
+
};
|
|
111
173
|
}
|
|
112
174
|
|
|
113
175
|
export function addMention(list: DocRef[], ref: DocRef): DocRef[] {
|
|
@@ -118,3 +180,5 @@ export function addMention(list: DocRef[], ref: DocRef): DocRef[] {
|
|
|
118
180
|
export function removeMention(list: DocRef[], ref: DocRef): DocRef[] {
|
|
119
181
|
return list.filter((x) => !sameDocRef(x, ref));
|
|
120
182
|
}
|
|
183
|
+
|
|
184
|
+
export { docKey };
|
package/src/chat/chat-helpers.ts
CHANGED
|
@@ -10,6 +10,18 @@ export type WriteToolOutput =
|
|
|
10
10
|
| { ok: true; change?: ChatChangeSummary; changes?: ChatChangeSummary[] }
|
|
11
11
|
| { ok: false; error: string; message: string };
|
|
12
12
|
|
|
13
|
+
export function isCheckoutWriteTool(name: string) {
|
|
14
|
+
return name === "writeFile" || name === "editFile" || name === "deleteFile";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A finished checkout tool changed the host, so the preview should load again. */
|
|
18
|
+
export function checkoutWriteReloads(name: string, output: unknown): boolean {
|
|
19
|
+
const body = output as { ok?: boolean } | undefined;
|
|
20
|
+
if (isCheckoutWriteTool(name)) return !body || body.ok !== false;
|
|
21
|
+
if (name === "runCommand") return body?.ok === true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export function isWriteTool(name: string) {
|
|
14
26
|
return (
|
|
15
27
|
name === "writeDoc" ||
|
|
@@ -39,7 +51,6 @@ const TREE_STATUSES = new Set<DocStatus>(["clean", "modified", "added", "conflic
|
|
|
39
51
|
|
|
40
52
|
/** Statuses a tool result wants applied to the local doc list immediately. */
|
|
41
53
|
export type TreeStatusPatch = {
|
|
42
|
-
locale: string;
|
|
43
54
|
type: string;
|
|
44
55
|
slug: string;
|
|
45
56
|
status: DocStatus;
|
|
@@ -55,7 +66,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
|
|
|
55
66
|
if (!row || typeof row !== "object") continue;
|
|
56
67
|
const item = row as Partial<TreeStatusPatch>;
|
|
57
68
|
if (
|
|
58
|
-
typeof item.locale !== "string" ||
|
|
59
69
|
typeof item.type !== "string" ||
|
|
60
70
|
!item.type ||
|
|
61
71
|
typeof item.slug !== "string" ||
|
|
@@ -66,7 +76,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
|
|
|
66
76
|
continue;
|
|
67
77
|
}
|
|
68
78
|
patches.push({
|
|
69
|
-
locale: item.locale,
|
|
70
79
|
type: item.type,
|
|
71
80
|
slug: item.slug,
|
|
72
81
|
status: item.status as DocStatus,
|
|
@@ -82,7 +91,6 @@ export function detailForTreePatch(
|
|
|
82
91
|
patch: TreeStatusPatch,
|
|
83
92
|
): {
|
|
84
93
|
workspaceId: string;
|
|
85
|
-
locale: string;
|
|
86
94
|
type: string;
|
|
87
95
|
slug: string;
|
|
88
96
|
status?: TreeStatusPatch["status"];
|
|
@@ -91,7 +99,6 @@ export function detailForTreePatch(
|
|
|
91
99
|
const gone = patch.absent === true || patch.status === "deleted";
|
|
92
100
|
return {
|
|
93
101
|
workspaceId,
|
|
94
|
-
locale: patch.locale,
|
|
95
102
|
type: patch.type,
|
|
96
103
|
slug: patch.slug,
|
|
97
104
|
...(gone ? { remove: true } : { status: patch.status }),
|
|
@@ -277,7 +284,7 @@ export function changeIdsInMessage(message: UIMessage): string[] {
|
|
|
277
284
|
|
|
278
285
|
export type ChangeStackItem = Pick<
|
|
279
286
|
ChatChangeSummary,
|
|
280
|
-
"id" | "
|
|
287
|
+
"id" | "type" | "slug" | "status" | "createdAt"
|
|
281
288
|
>;
|
|
282
289
|
|
|
283
290
|
function isLaterChange(a: ChangeStackItem, b: ChangeStackItem) {
|
|
@@ -295,10 +302,10 @@ export function presentedChangeStatus(
|
|
|
295
302
|
all: ChangeStackItem[],
|
|
296
303
|
): ChatChangeStatus {
|
|
297
304
|
if (change.status !== "applied") return change.status;
|
|
298
|
-
const key = `${change.
|
|
305
|
+
const key = `${change.type}/${change.slug}`;
|
|
299
306
|
const covered = all.some((other) => {
|
|
300
307
|
if (other.id === change.id) return false;
|
|
301
|
-
if (`${other.
|
|
308
|
+
if (`${other.type}/${other.slug}` !== key) return false;
|
|
302
309
|
if (other.status !== "applied" && other.status !== "kept") return false;
|
|
303
310
|
return isLaterChange(other, change);
|
|
304
311
|
});
|
package/src/chat/chat-state.ts
CHANGED
|
@@ -57,3 +57,14 @@ export function panelReducer(state: PanelState, action: PanelAction): PanelState
|
|
|
57
57
|
export function isPanelVisible(mode: PanelMode): boolean {
|
|
58
58
|
return mode === "open" || mode === "fullscreen";
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/** A docked column has no window chrome: it cannot close, minimize, or go fullscreen. */
|
|
62
|
+
export function allowsPanelAction(action: PanelAction, docked: boolean): boolean {
|
|
63
|
+
if (!docked) return true;
|
|
64
|
+
return (
|
|
65
|
+
action.type !== "toggle" &&
|
|
66
|
+
action.type !== "close" &&
|
|
67
|
+
action.type !== "minimize" &&
|
|
68
|
+
action.type !== "toggleFullscreen"
|
|
69
|
+
);
|
|
70
|
+
}
|
package/src/chrome/AppShell.tsx
CHANGED
|
@@ -7,17 +7,23 @@ import { ChatPanel } from "../chat/ChatPanel";
|
|
|
7
7
|
import { ChatProvider, ChatSessionScope } from "../chat/ChatProvider";
|
|
8
8
|
import { colors } from "../styles/tokens.stylex";
|
|
9
9
|
import { TooltipProvider } from "../ui/Tooltip";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cmsWorkspaceSlug,
|
|
12
|
+
isOrgPickerPath,
|
|
13
|
+
isWorkspaceCodePath,
|
|
14
|
+
isWorkspaceListPath,
|
|
15
|
+
} from "../workspace-path";
|
|
11
16
|
import { AppSidebar } from "./AppSidebar";
|
|
12
17
|
import { WorkspaceRail } from "./ContentExplorerSidebar";
|
|
13
18
|
import { CommandPalette } from "./CommandPalette";
|
|
14
|
-
import { FloatingUserMenu } from "./UserMenu";
|
|
19
|
+
import { CornerUserMenu, FloatingUserMenu } from "./UserMenu";
|
|
15
20
|
import { EditorActionsProvider } from "./EditorActionsContext";
|
|
16
21
|
import { EditorFocusProvider } from "./EditorFocusContext";
|
|
17
22
|
import { EditorStatusProvider } from "./EditorStatusContext";
|
|
18
23
|
import { WorkspaceNavProvider, useWorkspaceNav } from "./WorkspaceNavContext";
|
|
19
24
|
import { installParentChatHotkey } from "./hotkeys";
|
|
20
25
|
import { WorkspaceStatusBar } from "./WorkspaceStatusBar";
|
|
26
|
+
import { CodeSessionProvider } from "../pages/code-session-context";
|
|
21
27
|
|
|
22
28
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
|
23
29
|
return (
|
|
@@ -26,7 +32,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|
|
26
32
|
<EditorStatusProvider>
|
|
27
33
|
<EditorActionsProvider>
|
|
28
34
|
<EditorFocusProvider>
|
|
29
|
-
<
|
|
35
|
+
<CodeSessionProvider>
|
|
36
|
+
<AppShellInner>{children}</AppShellInner>
|
|
37
|
+
</CodeSessionProvider>
|
|
30
38
|
</EditorFocusProvider>
|
|
31
39
|
</EditorActionsProvider>
|
|
32
40
|
</EditorStatusProvider>
|
|
@@ -43,6 +51,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
43
51
|
const workspaceKey = cmsWorkspaceSlug(location.pathname);
|
|
44
52
|
const workspaceNav = useWorkspaceNav();
|
|
45
53
|
const orgPicker = isOrgPickerPath(location.pathname);
|
|
54
|
+
const workspaceList = isWorkspaceListPath(location.pathname);
|
|
46
55
|
const embed = searchParams.get("embed") === "1";
|
|
47
56
|
const [treeHost, setTreeHost] = useState<HTMLDivElement | null>(null);
|
|
48
57
|
const inWorkspace = Boolean(workspaceKey && !embed && !orgPicker);
|
|
@@ -52,8 +61,9 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
52
61
|
return installParentChatHotkey();
|
|
53
62
|
}, [embed]);
|
|
54
63
|
|
|
64
|
+
const codeSession = Boolean(workspaceKey && isWorkspaceCodePath(location.pathname));
|
|
55
65
|
const chat =
|
|
56
|
-
workspaceKey && !embed ? (
|
|
66
|
+
workspaceKey && !embed && !codeSession ? (
|
|
57
67
|
<ChatSessionScope>
|
|
58
68
|
<ChatBar />
|
|
59
69
|
<ChatPanel />
|
|
@@ -64,7 +74,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
64
74
|
<div {...stylex.props(styles.shell)}>
|
|
65
75
|
{workspaceKey && !embed ? <WorkspaceStatusBar workspaceId={workspaceKey} /> : null}
|
|
66
76
|
<div {...stylex.props(styles.body)}>
|
|
67
|
-
{orgPicker || embed ? null : inWorkspace ? (
|
|
77
|
+
{orgPicker || embed || workspaceList ? null : inWorkspace ? (
|
|
68
78
|
<WorkspaceRail workspace={workspaceNav} treeHost={treeHost} />
|
|
69
79
|
) : (
|
|
70
80
|
<AppSidebar />
|
|
@@ -78,6 +88,7 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
|
|
|
78
88
|
</div>
|
|
79
89
|
</div>
|
|
80
90
|
{orgPicker ? <FloatingUserMenu /> : null}
|
|
91
|
+
{workspaceList ? <CornerUserMenu /> : null}
|
|
81
92
|
{embed ? null : <CommandPalette />}
|
|
82
93
|
</div>
|
|
83
94
|
);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./hotkeys";
|
|
28
28
|
import { parsePreviewMessage } from "@backmatter/core/preview";
|
|
29
29
|
import { useWorkspaceNav } from "./WorkspaceNavContext";
|
|
30
|
+
import { codeSessionEntry } from "../code-session-entry";
|
|
30
31
|
import {
|
|
31
32
|
contentDocPath,
|
|
32
33
|
contentTypePath,
|
|
@@ -310,6 +311,17 @@ export function CommandPalette() {
|
|
|
310
311
|
});
|
|
311
312
|
}
|
|
312
313
|
if (workspaceNav) {
|
|
314
|
+
const codeHref = codeSessionEntry(workspaceNav.role, workspaceNav.workspaceSlug);
|
|
315
|
+
if (codeHref) {
|
|
316
|
+
list.push({
|
|
317
|
+
id: "code-session",
|
|
318
|
+
group: "Go to",
|
|
319
|
+
icon: "document",
|
|
320
|
+
label: "Code session",
|
|
321
|
+
hint: "Host and chat",
|
|
322
|
+
run: go(codeHref),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
313
325
|
list.push(
|
|
314
326
|
{
|
|
315
327
|
id: "settings-general",
|
|
@@ -357,7 +369,7 @@ export function CommandPalette() {
|
|
|
357
369
|
}
|
|
358
370
|
for (const doc of docs) {
|
|
359
371
|
list.push({
|
|
360
|
-
id: `doc-${doc.
|
|
372
|
+
id: `doc-${doc.type}-${doc.slug}`,
|
|
361
373
|
group: "Documents",
|
|
362
374
|
icon: "document",
|
|
363
375
|
label: `${doc.type}/${doc.slug}`,
|
|
@@ -370,15 +382,7 @@ export function CommandPalette() {
|
|
|
370
382
|
: doc.status === "modified"
|
|
371
383
|
? "Modified"
|
|
372
384
|
: doc.status,
|
|
373
|
-
run: go(
|
|
374
|
-
contentDocPath(
|
|
375
|
-
workspaceNav.workspaceSlug,
|
|
376
|
-
doc.type,
|
|
377
|
-
doc.slug,
|
|
378
|
-
doc.locale,
|
|
379
|
-
workspaceNav.defaultLocale,
|
|
380
|
-
),
|
|
381
|
-
),
|
|
385
|
+
run: go(contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug)),
|
|
382
386
|
});
|
|
383
387
|
}
|
|
384
388
|
}
|
|
@@ -16,13 +16,14 @@ import { Tooltip } from "../ui/Tooltip";
|
|
|
16
16
|
import {
|
|
17
17
|
contentDocPath,
|
|
18
18
|
contentRailHref,
|
|
19
|
-
|
|
19
|
+
isWorkspaceCodePath,
|
|
20
20
|
isWorkspaceEditorPath,
|
|
21
21
|
isWorkspaceMediaPath,
|
|
22
22
|
isWorkspaceSettingsPath,
|
|
23
23
|
mediaPath,
|
|
24
24
|
settingsPath,
|
|
25
25
|
} from "../workspace-path";
|
|
26
|
+
import { codeSessionEntry } from "../code-session-entry";
|
|
26
27
|
import { UserMenu } from "./UserMenu";
|
|
27
28
|
import {
|
|
28
29
|
groupContentTypes,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
ExplorerHeaderListIcon,
|
|
39
40
|
ExplorerHeaderNewFileIcon,
|
|
40
41
|
ExplorerTreeNewFileIcon,
|
|
42
|
+
ExplorerRailCodeIcon,
|
|
41
43
|
ExplorerRailFilesIcon,
|
|
42
44
|
ExplorerRailHistoryIcon,
|
|
43
45
|
ExplorerRailMediaIcon,
|
|
@@ -62,6 +64,7 @@ import {
|
|
|
62
64
|
} from "./folder-create-context";
|
|
63
65
|
import { canWriteContent } from "../workspace/permissions";
|
|
64
66
|
import {
|
|
67
|
+
contentRailCurrent,
|
|
65
68
|
contentTreeVisible,
|
|
66
69
|
toggleContentTree,
|
|
67
70
|
useContentTreeOpen,
|
|
@@ -264,6 +267,7 @@ function ContentExplorerSidebar({
|
|
|
264
267
|
pendingCount={pendingCount}
|
|
265
268
|
settingsActive={settingsActive}
|
|
266
269
|
workspaceSlug={workspace.workspaceSlug}
|
|
270
|
+
role={workspace.role}
|
|
267
271
|
treeOpen={treeOpen}
|
|
268
272
|
/>
|
|
269
273
|
{tree && treeHost ? createPortal(tree, treeHost) : null}
|
|
@@ -271,22 +275,26 @@ function ContentExplorerSidebar({
|
|
|
271
275
|
);
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
function ActivityRail({
|
|
278
|
+
export function ActivityRail({
|
|
275
279
|
pendingCount,
|
|
276
280
|
settingsActive,
|
|
277
281
|
workspaceSlug,
|
|
282
|
+
role,
|
|
278
283
|
treeOpen,
|
|
279
284
|
}: {
|
|
280
285
|
pendingCount: number;
|
|
281
286
|
settingsActive: boolean;
|
|
282
287
|
workspaceSlug: string;
|
|
288
|
+
role: string;
|
|
283
289
|
treeOpen: boolean;
|
|
284
290
|
}) {
|
|
285
291
|
const navigate = useNavigate();
|
|
286
292
|
const location = useLocation();
|
|
287
293
|
const onDocument = isWorkspaceEditorPath(location.pathname);
|
|
288
294
|
const filesLabel = onDocument ? (treeOpen ? "Hide files" : "Show files") : "Content";
|
|
289
|
-
const filesActive =
|
|
295
|
+
const filesActive = contentRailCurrent(location.pathname, treeOpen);
|
|
296
|
+
const codeHref = codeSessionEntry(role, workspaceSlug);
|
|
297
|
+
const codeActive = isWorkspaceCodePath(location.pathname);
|
|
290
298
|
|
|
291
299
|
return (
|
|
292
300
|
<nav
|
|
@@ -351,6 +359,18 @@ function ActivityRail({
|
|
|
351
359
|
<ExplorerRailMediaIcon />
|
|
352
360
|
</Link>
|
|
353
361
|
</Tooltip>
|
|
362
|
+
{codeHref ? (
|
|
363
|
+
<Tooltip label="Code session" side="right">
|
|
364
|
+
<Link
|
|
365
|
+
to={codeHref}
|
|
366
|
+
aria-label="Code session"
|
|
367
|
+
aria-current={codeActive ? "page" : undefined}
|
|
368
|
+
{...stylex.props(styles.railBtn, codeActive && styles.railBtnActive)}
|
|
369
|
+
>
|
|
370
|
+
<ExplorerRailCodeIcon />
|
|
371
|
+
</Link>
|
|
372
|
+
</Tooltip>
|
|
373
|
+
) : null}
|
|
354
374
|
</div>
|
|
355
375
|
<div {...stylex.props(styles.railBottom)}>
|
|
356
376
|
<Tooltip label="Settings" side="right">
|
package/src/chrome/UserMenu.tsx
CHANGED
|
@@ -16,10 +16,28 @@ export function FloatingUserMenu() {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Small account control for full-screen pages that have no sidebar. */
|
|
20
|
+
export function CornerUserMenu() {
|
|
21
|
+
return (
|
|
22
|
+
<div {...stylex.props(styles.corner)}>
|
|
23
|
+
<UserMenu quiet placement="below-end" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function UserMenu({
|
|
29
|
+
quiet = false,
|
|
30
|
+
compact = false,
|
|
31
|
+
placement = "above-start",
|
|
32
|
+
}: {
|
|
33
|
+
quiet?: boolean;
|
|
34
|
+
compact?: boolean;
|
|
35
|
+
placement?: "above-start" | "below-end";
|
|
36
|
+
}) {
|
|
20
37
|
const { email, onLogout } = useAppChrome();
|
|
21
38
|
const location = useLocation();
|
|
22
39
|
const onAccount = location.pathname.replace(/\/$/, "") === "/account";
|
|
40
|
+
const belowEnd = placement === "below-end";
|
|
23
41
|
|
|
24
42
|
return (
|
|
25
43
|
<Menu.Root>
|
|
@@ -29,6 +47,7 @@ export function UserMenu({ quiet = false, compact = false }: { quiet?: boolean;
|
|
|
29
47
|
styles.trigger,
|
|
30
48
|
quiet && styles.triggerQuiet,
|
|
31
49
|
compact && styles.triggerCompact,
|
|
50
|
+
belowEnd && styles.triggerCorner,
|
|
32
51
|
onAccount && styles.triggerActive,
|
|
33
52
|
)}
|
|
34
53
|
>
|
|
@@ -40,8 +59,8 @@ export function UserMenu({ quiet = false, compact = false }: { quiet?: boolean;
|
|
|
40
59
|
</Menu.Trigger>
|
|
41
60
|
<Menu.Portal>
|
|
42
61
|
<Menu.Positioner
|
|
43
|
-
side="top"
|
|
44
|
-
align={compact ? "center" : "start"}
|
|
62
|
+
side={belowEnd ? "bottom" : "top"}
|
|
63
|
+
align={belowEnd ? "end" : compact ? "center" : "start"}
|
|
45
64
|
sideOffset={6}
|
|
46
65
|
className={cx(popup.positioner)}
|
|
47
66
|
>
|
|
@@ -81,6 +100,13 @@ const styles = stylex.create({
|
|
|
81
100
|
zIndex: z.dropdown,
|
|
82
101
|
maxWidth: "220px",
|
|
83
102
|
},
|
|
103
|
+
corner: {
|
|
104
|
+
position: "fixed",
|
|
105
|
+
top: space.lg,
|
|
106
|
+
right: space.lg,
|
|
107
|
+
zIndex: z.dropdown,
|
|
108
|
+
maxWidth: "240px",
|
|
109
|
+
},
|
|
84
110
|
trigger: {
|
|
85
111
|
display: "flex",
|
|
86
112
|
width: "100%",
|
|
@@ -128,6 +154,11 @@ const styles = stylex.create({
|
|
|
128
154
|
fontSize: type.sm,
|
|
129
155
|
fontWeight: 500,
|
|
130
156
|
},
|
|
157
|
+
triggerCorner: {
|
|
158
|
+
minHeight: "44px",
|
|
159
|
+
justifyContent: "flex-end",
|
|
160
|
+
textAlign: "right",
|
|
161
|
+
},
|
|
131
162
|
triggerActive: {
|
|
132
163
|
backgroundColor: colors.bgActive,
|
|
133
164
|
},
|