@backmatter/studio 0.0.5 → 0.0.7
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/{EmbedPublishPage-hx1xoAhI.js → EmbedPublishPage-88VsmOhi.js} +13 -13
- package/dist/chunks/{UiKitPage-BOunq6mr.js → UiKitPage-X4hkFL2d.js} +9 -9
- package/dist/chunks/{WorkspaceRoutes-CTFTWu3L.js → WorkspaceRoutes-DXzdWqN3.js} +20334 -18542
- package/dist/chunks/{dashboard-upmiawx5.js → dashboard-DCHRI0WD.js} +422 -400
- package/dist/chunks/{index-CBvevwfT.js → index-BS0-Bvbk.js} +2 -2
- package/dist/chunks/{status-bar-action-DiXhfcj3.js → status-bar-action-wj1KZ51m.js} +6925 -6567
- package/dist/dashboard.js +2 -2
- package/dist/index.js +18 -18
- package/dist/site-widget.js +788 -234
- package/dist/studio.css +454 -66
- package/package.json +3 -2
- package/src/App.tsx +2 -0
- package/src/WorkspaceRoutes.tsx +4 -0
- package/src/api.ts +104 -0
- package/src/chat/ChatMessage.tsx +10 -3
- package/src/chat/ChatPanel.tsx +1 -1
- package/src/chat/Composer.tsx +18 -18
- package/src/chat/chat-helpers.ts +58 -1
- package/src/chrome/AppSidebar.tsx +31 -156
- package/src/chrome/CommandPalette.tsx +211 -74
- package/src/chrome/ContentExplorerSidebar.tsx +1174 -0
- package/src/chrome/UserMenu.tsx +18 -3
- package/src/chrome/WorkspaceNavContext.tsx +1 -0
- package/src/chrome/command-palette-icons.tsx +152 -0
- package/src/chrome/content-path-tree.ts +95 -0
- package/src/chrome/content-repo-path.ts +52 -0
- package/src/chrome/explorer-icons.tsx +89 -0
- package/src/chrome/folder-create-context.ts +147 -0
- package/src/chrome/icons.tsx +13 -0
- package/src/pages/BlockSettingsDialog.tsx +4 -0
- package/src/pages/CliAuthorizePage.tsx +43 -0
- package/src/pages/DocEditorPage.tsx +33 -4
- package/src/pages/MediaFieldInput.tsx +139 -0
- package/src/pages/MediaGalleryPage.tsx +752 -0
- package/src/pages/MediaLightbox.tsx +460 -0
- package/src/pages/SettingsLayout.tsx +7 -1
- package/src/pages/SettingsUsagePage.tsx +187 -0
- package/src/pages/TypeDocsPage.tsx +2 -210
- package/src/pages/WorkspaceLayout.tsx +6 -1
- package/src/pages/media-grid.ts +190 -0
- package/src/pages/preview-url.ts +13 -2
- package/src/preview-frame.tsx +78 -0
- package/src/replica/useReplica.ts +9 -0
- package/src/site-bar.tsx +769 -0
- package/src/site-widget.tsx +33 -162
- package/src/ui/ConfirmDialog.tsx +1 -1
- package/src/workspace/permissions.ts +9 -0
- package/src/workspace-path.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "The Backmatter editing surface: document editor, AI chat, publish. Mounted on your site or hosted on the dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -52,8 +52,9 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@ai-sdk/react": "^4.0.100",
|
|
55
|
-
"@backmatter/core": "^0.0.
|
|
55
|
+
"@backmatter/core": "^0.0.24",
|
|
56
56
|
"@base-ui/react": "^1.6.0",
|
|
57
|
+
"@central-icons-react/round-outlined-radius-2-stroke-1.5": "1.2.15",
|
|
57
58
|
"@dnd-kit/abstract": "^0.5.0",
|
|
58
59
|
"@dnd-kit/react": "^0.5.0",
|
|
59
60
|
"@fontsource/geist-mono": "^5.3.0",
|
package/src/App.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { NewWorkspacePage } from "./pages/NewWorkspacePage";
|
|
|
19
19
|
import { AcceptInvitePage } from "./pages/AcceptInvitePage";
|
|
20
20
|
import { LegacyWorkspaceRedirect } from "./pages/LegacyWorkspaceRedirect";
|
|
21
21
|
import { UserSettingsPage } from "./pages/UserSettingsPage";
|
|
22
|
+
import { CliAuthorizePage } from "./pages/CliAuthorizePage";
|
|
22
23
|
import { BootstrapSplash } from "./replica/BootstrapSplash";
|
|
23
24
|
import { ReplicaGate, ReplicaProvider } from "./replica/ReplicaProvider";
|
|
24
25
|
import { replicaStore } from "./replica/instance";
|
|
@@ -137,6 +138,7 @@ function AppInner() {
|
|
|
137
138
|
<Route path="/workspaces/new" element={<NewWorkspacePage />} />
|
|
138
139
|
<Route path="/invite" element={<AcceptInvitePage />} />
|
|
139
140
|
<Route path="/account" element={<UserSettingsPage />} />
|
|
141
|
+
<Route path="/cli/authorize" element={<CliAuthorizePage />} />
|
|
140
142
|
<Route
|
|
141
143
|
path="/workspaces/:workspaceId/*"
|
|
142
144
|
element={<LegacyWorkspaceRedirect />}
|
package/src/WorkspaceRoutes.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { WorkspaceLayout } from "./pages/WorkspaceLayout";
|
|
|
6
6
|
import { WorkspaceHomeRedirect } from "./pages/WorkspaceHomeRedirect";
|
|
7
7
|
import { TypeDocsPage } from "./pages/TypeDocsPage";
|
|
8
8
|
import { SettingsGeneralPage } from "./pages/SettingsGeneralPage";
|
|
9
|
+
import { SettingsUsagePage } from "./pages/SettingsUsagePage";
|
|
9
10
|
import { SettingsGithubPage } from "./pages/SettingsGithubPage";
|
|
10
11
|
import { SettingsIndexRedirect, SettingsLayout } from "./pages/SettingsLayout";
|
|
11
12
|
import { SettingsSchemaPage } from "./pages/SettingsSchemaPage";
|
|
@@ -13,6 +14,7 @@ import { SettingsMcpPage } from "./pages/SettingsMcpPage";
|
|
|
13
14
|
import { SettingsUsersPage } from "./pages/SettingsUsersPage";
|
|
14
15
|
import { GithubConnectPage } from "./pages/GithubConnectPage";
|
|
15
16
|
import { DocEditorPage } from "./pages/DocEditorPage";
|
|
17
|
+
import { MediaGalleryPage } from "./pages/MediaGalleryPage";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Returns `<Route>` elements. Spread the result inside a `<Routes>`; react-router
|
|
@@ -26,6 +28,7 @@ export function workspaceRoutes() {
|
|
|
26
28
|
<Route element={<SettingsLayout />}>
|
|
27
29
|
<Route path="settings" element={<SettingsIndexRedirect />} />
|
|
28
30
|
<Route path="settings/general" element={<SettingsGeneralPage />} />
|
|
31
|
+
<Route path="settings/usage" element={<SettingsUsagePage />} />
|
|
29
32
|
<Route path="settings/github" element={<SettingsGithubPage />} />
|
|
30
33
|
<Route path="settings/schema" element={<SettingsSchemaPage />} />
|
|
31
34
|
<Route path="settings/mcp" element={<SettingsMcpPage />} />
|
|
@@ -33,6 +36,7 @@ export function workspaceRoutes() {
|
|
|
33
36
|
<Route path="github" element={<GithubConnectPage />} />
|
|
34
37
|
</Route>
|
|
35
38
|
<Route path="content/:type/:docSlug" element={<DocEditorPage />} />
|
|
39
|
+
<Route path="media" element={<MediaGalleryPage />} />
|
|
36
40
|
</Route>
|
|
37
41
|
);
|
|
38
42
|
}
|
package/src/api.ts
CHANGED
|
@@ -169,10 +169,49 @@ export type WorkspaceDetail = {
|
|
|
169
169
|
hasHostToken: boolean;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
export type WorkspaceUsage = {
|
|
173
|
+
period: string;
|
|
174
|
+
dataRequests: {
|
|
175
|
+
public: number;
|
|
176
|
+
studio: number;
|
|
177
|
+
replica: number;
|
|
178
|
+
preview: number;
|
|
179
|
+
ai: number;
|
|
180
|
+
};
|
|
181
|
+
work: {
|
|
182
|
+
commits: number;
|
|
183
|
+
commitFiles: number;
|
|
184
|
+
imports: number;
|
|
185
|
+
importFiles: number;
|
|
186
|
+
saves: number;
|
|
187
|
+
mediaTransforms: number;
|
|
188
|
+
};
|
|
189
|
+
ai: { inputTokens: number; outputTokens: number };
|
|
190
|
+
members: number;
|
|
191
|
+
mediaBytes: number;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type CatalogMedia = {
|
|
195
|
+
id: string;
|
|
196
|
+
filename: string;
|
|
197
|
+
contentType: string;
|
|
198
|
+
byteSize: number;
|
|
199
|
+
width: number;
|
|
200
|
+
height: number;
|
|
201
|
+
url: string;
|
|
202
|
+
thumbUrl: string;
|
|
203
|
+
variants: Record<string, string>;
|
|
204
|
+
createdAt: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
172
207
|
export function fetchWorkspace(workspaceId: string) {
|
|
173
208
|
return api<{ workspace: WorkspaceDetail }>(`/workspaces/${workspaceId}`);
|
|
174
209
|
}
|
|
175
210
|
|
|
211
|
+
export function fetchWorkspaceUsage(workspaceId: string) {
|
|
212
|
+
return api<WorkspaceUsage>(`/workspaces/${workspaceId}/usage`);
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
export function fetchReplica(workspaceId: string, cache?: ReplicaCache) {
|
|
177
216
|
const query = cache ? `?${replicaQuery(cache)}` : "";
|
|
178
217
|
return api<ReplicaBody>(`/workspaces/${workspaceId}/replica${query}`);
|
|
@@ -829,6 +868,71 @@ export function deleteWorkspaceMcp(workspaceId: string, id: string) {
|
|
|
829
868
|
});
|
|
830
869
|
}
|
|
831
870
|
|
|
871
|
+
export function fetchMedia(
|
|
872
|
+
workspaceId: string,
|
|
873
|
+
input: { query?: string; cursor?: string } = {},
|
|
874
|
+
) {
|
|
875
|
+
const params = new URLSearchParams();
|
|
876
|
+
if (input.query) params.set("q", input.query);
|
|
877
|
+
if (input.cursor) params.set("cursor", input.cursor);
|
|
878
|
+
const query = params.size ? `?${params}` : "";
|
|
879
|
+
return api<{ total: number; media: CatalogMedia[]; nextCursor: string | null }>(
|
|
880
|
+
`/workspaces/${workspaceId}/media${query}`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export function uploadMedia(
|
|
885
|
+
workspaceId: string,
|
|
886
|
+
file: File,
|
|
887
|
+
onProgress?: (fraction: number) => void,
|
|
888
|
+
) {
|
|
889
|
+
return new Promise<{ media: CatalogMedia }>((resolve, reject) => {
|
|
890
|
+
const xhr = new XMLHttpRequest();
|
|
891
|
+
xhr.open("POST", apiUrl(`/workspaces/${workspaceId}/media`));
|
|
892
|
+
xhr.withCredentials = apiCredentials() === "include";
|
|
893
|
+
for (const [name, value] of Object.entries(apiHeaders())) {
|
|
894
|
+
xhr.setRequestHeader(name, value);
|
|
895
|
+
}
|
|
896
|
+
xhr.upload.onprogress = (event) => {
|
|
897
|
+
if (!onProgress || !event.lengthComputable || event.total <= 0) return;
|
|
898
|
+
onProgress(event.loaded / event.total);
|
|
899
|
+
};
|
|
900
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
901
|
+
xhr.onload = () => {
|
|
902
|
+
const payload = (() => {
|
|
903
|
+
try {
|
|
904
|
+
return JSON.parse(xhr.responseText) as { media?: CatalogMedia; error?: string };
|
|
905
|
+
} catch {
|
|
906
|
+
return {};
|
|
907
|
+
}
|
|
908
|
+
})();
|
|
909
|
+
if (xhr.status === 401 || xhr.status === 426) {
|
|
910
|
+
reportUnauthorized(xhr.status, payload.error);
|
|
911
|
+
}
|
|
912
|
+
if (xhr.status < 200 || xhr.status >= 300 || !payload.media) {
|
|
913
|
+
const err = new Error(payload.error ?? `upload failed: ${xhr.status}`) as ApiError;
|
|
914
|
+
err.status = xhr.status;
|
|
915
|
+
err.code = payload.error;
|
|
916
|
+
reject(err);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
onProgress?.(1);
|
|
920
|
+
resolve({ media: payload.media });
|
|
921
|
+
};
|
|
922
|
+
const body = new FormData();
|
|
923
|
+
body.set("file", file);
|
|
924
|
+
xhr.send(body);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function deleteMedia(workspaceId: string, mediaId: string) {
|
|
929
|
+
return api<{ ok: true }>(`/workspaces/${workspaceId}/media/${mediaId}`, { method: "DELETE" });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function approveCliLogin(id: string) {
|
|
933
|
+
return api<{ ok: true }>(`/cli/login/${id}/approve`, { method: "POST" });
|
|
934
|
+
}
|
|
935
|
+
|
|
832
936
|
export function testWorkspaceMcp(workspaceId: string, id: string) {
|
|
833
937
|
return api<{ connector: McpConnector }>(
|
|
834
938
|
`/workspaces/${workspaceId}/mcp/${id}/test`,
|
package/src/chat/ChatMessage.tsx
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
bindDocsPlaceholders,
|
|
13
13
|
isCommitTool,
|
|
14
14
|
isWriteTool,
|
|
15
|
+
presentedChangeStatus,
|
|
15
16
|
replaceEchoedDocList,
|
|
16
17
|
splitDocsPlaceholders,
|
|
18
|
+
stackChanges,
|
|
17
19
|
toolLabel,
|
|
18
20
|
trimPartialPlaceholder,
|
|
19
21
|
type WriteToolOutput,
|
|
@@ -137,9 +139,13 @@ export function ChatMessage({
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
const changeIds = session.changeIdsForMessage(message);
|
|
142
|
+
const stack = stackChanges(session.messages, session.changes);
|
|
140
143
|
const applied = changeIds
|
|
141
144
|
.map((id) => session.changes[id])
|
|
142
|
-
.filter(
|
|
145
|
+
.filter(
|
|
146
|
+
(c): c is ChatChangeSummary =>
|
|
147
|
+
Boolean(c) && presentedChangeStatus(c, stack) === "applied",
|
|
148
|
+
);
|
|
143
149
|
const streaming = isLast && live;
|
|
144
150
|
|
|
145
151
|
async function act(id: string, fn: () => Promise<void>) {
|
|
@@ -242,6 +248,7 @@ export function ChatMessage({
|
|
|
242
248
|
tp.state === "output-available" && !inlined.has(i) && !streaming
|
|
243
249
|
? docListFromTool(name, tp.output)
|
|
244
250
|
: null;
|
|
251
|
+
const viewStatus = change ? presentedChangeStatus(change, stack) : null;
|
|
245
252
|
return (
|
|
246
253
|
<div key={tp.toolCallId} {...stylex.props(styles.toolBlock)}>
|
|
247
254
|
<div {...stylex.props(styles.toolRow, failed && styles.toolFailed)}>
|
|
@@ -263,9 +270,9 @@ export function ChatMessage({
|
|
|
263
270
|
emptyLabel={name === "searchDocs" ? "No matches" : "No documents"}
|
|
264
271
|
/>
|
|
265
272
|
) : null}
|
|
266
|
-
{change ? (
|
|
273
|
+
{change && viewStatus ? (
|
|
267
274
|
<ChangeCard
|
|
268
|
-
change={change}
|
|
275
|
+
change={{ ...change, status: viewStatus }}
|
|
269
276
|
workspaceId={workspaceId}
|
|
270
277
|
busy={busyId === change.id}
|
|
271
278
|
canAct={canAct && !streaming}
|
package/src/chat/ChatPanel.tsx
CHANGED
package/src/chat/Composer.tsx
CHANGED
|
@@ -257,6 +257,24 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
257
257
|
</button>
|
|
258
258
|
</Tooltip>
|
|
259
259
|
<span {...stylex.props(styles.spacer)} />
|
|
260
|
+
{ui.pointer ? (
|
|
261
|
+
<Tooltip label={pointing ? "Stop pointing" : "Point at an element on the page"}>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
aria-label="Point at an element on the page"
|
|
265
|
+
aria-pressed={pointing}
|
|
266
|
+
disabled={ui.aiAvailable !== true}
|
|
267
|
+
onClick={() => {
|
|
268
|
+
const next = !pointing;
|
|
269
|
+
setPointing(next);
|
|
270
|
+
postPointer(next);
|
|
271
|
+
}}
|
|
272
|
+
{...stylex.props(styles.iconBtn, pointing && styles.iconBtnActive)}
|
|
273
|
+
>
|
|
274
|
+
<PointerIcon />
|
|
275
|
+
</button>
|
|
276
|
+
</Tooltip>
|
|
277
|
+
) : null}
|
|
260
278
|
{ui.focus ? (
|
|
261
279
|
<Tooltip
|
|
262
280
|
label={
|
|
@@ -277,24 +295,6 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
277
295
|
</button>
|
|
278
296
|
</Tooltip>
|
|
279
297
|
) : null}
|
|
280
|
-
{ui.pointer ? (
|
|
281
|
-
<Tooltip label={pointing ? "Stop pointing" : "Point at an element on the page"}>
|
|
282
|
-
<button
|
|
283
|
-
type="button"
|
|
284
|
-
aria-label="Point at an element on the page"
|
|
285
|
-
aria-pressed={pointing}
|
|
286
|
-
disabled={ui.aiAvailable !== true}
|
|
287
|
-
onClick={() => {
|
|
288
|
-
const next = !pointing;
|
|
289
|
-
setPointing(next);
|
|
290
|
-
postPointer(next);
|
|
291
|
-
}}
|
|
292
|
-
{...stylex.props(styles.iconBtn, pointing && styles.iconBtnActive)}
|
|
293
|
-
>
|
|
294
|
-
<PointerIcon />
|
|
295
|
-
</button>
|
|
296
|
-
</Tooltip>
|
|
297
|
-
) : null}
|
|
298
298
|
<ModelPicker />
|
|
299
299
|
{busy ? (
|
|
300
300
|
<Tooltip label="Stop generating">
|
package/src/chat/chat-helpers.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Pure helpers used by chat components. Kept free of StyleX/React so they can
|
|
4
4
|
// be unit tested with `bun test`.
|
|
5
5
|
import { getToolName, isToolUIPart, type UIMessage } from "ai";
|
|
6
|
-
import type { ChatChangeSummary, ChatThread, DocStatus } from "../api";
|
|
6
|
+
import type { ChatChangeSummary, ChatThread, DocStatus, ChatChangeStatus } from "../api";
|
|
7
7
|
import type { DocRef } from "../chrome/EditorFocusContext";
|
|
8
8
|
|
|
9
9
|
export type WriteToolOutput =
|
|
@@ -186,6 +186,63 @@ export function changeIdsInMessage(message: UIMessage): string[] {
|
|
|
186
186
|
return ids;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export type ChangeStackItem = Pick<
|
|
190
|
+
ChatChangeSummary,
|
|
191
|
+
"id" | "locale" | "type" | "slug" | "status" | "createdAt"
|
|
192
|
+
>;
|
|
193
|
+
|
|
194
|
+
function isLaterChange(a: ChangeStackItem, b: ChangeStackItem) {
|
|
195
|
+
if (a.createdAt !== b.createdAt) return a.createdAt > b.createdAt;
|
|
196
|
+
return a.id > b.id;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Status to show on a change card. Edits are already in the working copy, so
|
|
201
|
+
* only the latest applied-or-kept edit of a document offers Keep and Undo.
|
|
202
|
+
* An earlier applied edit reads as kept until that later edit is undone.
|
|
203
|
+
*/
|
|
204
|
+
export function presentedChangeStatus(
|
|
205
|
+
change: ChangeStackItem,
|
|
206
|
+
all: ChangeStackItem[],
|
|
207
|
+
): ChatChangeStatus {
|
|
208
|
+
if (change.status !== "applied") return change.status;
|
|
209
|
+
const key = `${change.locale}/${change.type}/${change.slug}`;
|
|
210
|
+
const covered = all.some((other) => {
|
|
211
|
+
if (other.id === change.id) return false;
|
|
212
|
+
if (`${other.locale}/${other.type}/${other.slug}` !== key) return false;
|
|
213
|
+
if (other.status !== "applied" && other.status !== "kept") return false;
|
|
214
|
+
return isLaterChange(other, change);
|
|
215
|
+
});
|
|
216
|
+
return covered ? "kept" : "applied";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Write-tool changes in the thread, with session status winning over the tool output. */
|
|
220
|
+
export function stackChanges(
|
|
221
|
+
messages: UIMessage[],
|
|
222
|
+
overrides: Record<string, ChatChangeSummary>,
|
|
223
|
+
): ChatChangeSummary[] {
|
|
224
|
+
const byId = new Map<string, ChatChangeSummary>();
|
|
225
|
+
for (const message of messages) {
|
|
226
|
+
for (const part of message.parts) {
|
|
227
|
+
if (!isToolUIPart(part) || part.state !== "output-available") continue;
|
|
228
|
+
if (!isWriteTool(getToolName(part))) continue;
|
|
229
|
+
const out = part.output as WriteToolOutput | undefined;
|
|
230
|
+
if (!out?.ok) continue;
|
|
231
|
+
byId.set(out.change.id, out.change);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const change of Object.values(overrides)) {
|
|
235
|
+
const existing = byId.get(change.id);
|
|
236
|
+
byId.set(
|
|
237
|
+
change.id,
|
|
238
|
+
existing
|
|
239
|
+
? { ...existing, ...change, summary: change.summary ?? existing.summary }
|
|
240
|
+
: change,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return [...byId.values()];
|
|
244
|
+
}
|
|
245
|
+
|
|
189
246
|
/** Find an active `@query` token ending at the caret. */
|
|
190
247
|
export function mentionQueryAt(
|
|
191
248
|
text: string,
|
|
@@ -4,38 +4,27 @@ import * as stylex from "@stylexjs/stylex";
|
|
|
4
4
|
import { useReplicaState } from "../replica/useReplica";
|
|
5
5
|
import { colors, space, type } from "../styles/tokens.stylex";
|
|
6
6
|
import { useWorkspaceNav } from "./WorkspaceNavContext";
|
|
7
|
-
import {
|
|
8
|
-
import { useStudioDefinition } from "../define-studio";
|
|
7
|
+
import { NavItem } from "./NavItem";
|
|
9
8
|
import { UserMenu } from "./UserMenu";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
contentDocPath,
|
|
14
|
-
contentTypePath,
|
|
15
|
-
isWorkspaceSettingsPath,
|
|
16
|
-
settingsPath,
|
|
17
|
-
workspaceHomePath,
|
|
18
|
-
} from "../workspace-path";
|
|
19
|
-
import { DocIcon, ExternalIcon, GearIcon } from "./icons";
|
|
20
|
-
import {
|
|
21
|
-
groupContentTypes,
|
|
22
|
-
modelHasLocale,
|
|
23
|
-
type ContentTypeNav,
|
|
24
|
-
} from "./content-type-groups";
|
|
25
|
-
|
|
9
|
+
import { cmsWorkspaceSlug, isWorkspaceMediaPath, workspaceHomePath } from "../workspace-path";
|
|
10
|
+
import { DocIcon } from "./icons";
|
|
11
|
+
import { ContentExplorerSidebar } from "./ContentExplorerSidebar";
|
|
26
12
|
export function AppSidebar() {
|
|
27
13
|
const location = useLocation();
|
|
28
14
|
const workspaceNav = useWorkspaceNav();
|
|
29
15
|
const inWorkspace = Boolean(cmsWorkspaceSlug(location.pathname));
|
|
16
|
+
const railOnly = inWorkspace && isWorkspaceMediaPath(location.pathname);
|
|
30
17
|
|
|
31
18
|
return (
|
|
32
|
-
<aside {...stylex.props(styles.aside)}>
|
|
19
|
+
<aside {...stylex.props(styles.aside, railOnly && styles.asideRail)}>
|
|
33
20
|
{inWorkspace && workspaceNav ? (
|
|
34
|
-
<
|
|
21
|
+
<div {...stylex.props(styles.workspaceNav)}>
|
|
22
|
+
<ContentExplorerSidebar workspace={workspaceNav} />
|
|
23
|
+
</div>
|
|
35
24
|
) : (
|
|
36
25
|
<WorkspaceListNav />
|
|
37
26
|
)}
|
|
38
|
-
<SidebarFooter />
|
|
27
|
+
<SidebarFooter compact={railOnly} />
|
|
39
28
|
</aside>
|
|
40
29
|
);
|
|
41
30
|
}
|
|
@@ -75,157 +64,40 @@ function WorkspaceListNav() {
|
|
|
75
64
|
);
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
defaultLocale: string,
|
|
81
|
-
model: ContentTypeNav,
|
|
82
|
-
) {
|
|
83
|
-
if (model.singleton && !modelHasLocale(model.path)) {
|
|
84
|
-
return contentDocPath(
|
|
85
|
-
workspaceSlug,
|
|
86
|
-
model.type,
|
|
87
|
-
IMPLICIT_SINGLETON_SLUG,
|
|
88
|
-
"",
|
|
89
|
-
defaultLocale,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
return contentTypePath(workspaceSlug, model.type);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function TypeList({
|
|
96
|
-
models,
|
|
97
|
-
workspaceSlug,
|
|
98
|
-
defaultLocale,
|
|
99
|
-
}: {
|
|
100
|
-
models: ContentTypeNav[];
|
|
101
|
-
workspaceSlug: string;
|
|
102
|
-
defaultLocale: string;
|
|
103
|
-
}) {
|
|
104
|
-
return (
|
|
105
|
-
<ul {...stylex.props(styles.list)}>
|
|
106
|
-
{models.map((model) => (
|
|
107
|
-
<li key={model.type}>
|
|
108
|
-
<NavItem to={typeHref(workspaceSlug, defaultLocale, model)}>
|
|
109
|
-
<DocIcon />
|
|
110
|
-
<span {...stylex.props(styles.truncate, styles.capitalize)}>
|
|
111
|
-
{model.type}
|
|
112
|
-
</span>
|
|
113
|
-
</NavItem>
|
|
114
|
-
</li>
|
|
115
|
-
))}
|
|
116
|
-
</ul>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function WorkspaceNav({
|
|
121
|
-
workspace,
|
|
122
|
-
}: {
|
|
123
|
-
workspace: {
|
|
124
|
-
workspaceSlug: string;
|
|
125
|
-
defaultLocale: string;
|
|
126
|
-
contentTypes: string[];
|
|
127
|
-
contentModels: ContentTypeNav[];
|
|
128
|
-
};
|
|
129
|
-
}) {
|
|
130
|
-
const location = useLocation();
|
|
131
|
-
const grouped = groupContentTypes(workspace.contentModels);
|
|
132
|
-
const empty = workspace.contentTypes.length === 0;
|
|
133
|
-
const menu = useStudioDefinition().menu ?? [];
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<>
|
|
137
|
-
<nav aria-label="Content" {...stylex.props(styles.nav)}>
|
|
138
|
-
{empty ? (
|
|
139
|
-
<div>
|
|
140
|
-
<SectionLabel>Content</SectionLabel>
|
|
141
|
-
<p {...stylex.props(styles.muted)}>
|
|
142
|
-
No content types yet. Pull from GitHub to load models.
|
|
143
|
-
</p>
|
|
144
|
-
</div>
|
|
145
|
-
) : (
|
|
146
|
-
<>
|
|
147
|
-
{grouped.pages.length > 0 ? (
|
|
148
|
-
<div>
|
|
149
|
-
<SectionLabel>Pages</SectionLabel>
|
|
150
|
-
<TypeList
|
|
151
|
-
models={grouped.pages}
|
|
152
|
-
workspaceSlug={workspace.workspaceSlug}
|
|
153
|
-
defaultLocale={workspace.defaultLocale}
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
) : null}
|
|
157
|
-
{grouped.data.length > 0 ? (
|
|
158
|
-
<div>
|
|
159
|
-
<SectionLabel>Data</SectionLabel>
|
|
160
|
-
<TypeList
|
|
161
|
-
models={grouped.data}
|
|
162
|
-
workspaceSlug={workspace.workspaceSlug}
|
|
163
|
-
defaultLocale={workspace.defaultLocale}
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
166
|
-
) : null}
|
|
167
|
-
{grouped.globals.length > 0 ? (
|
|
168
|
-
<div>
|
|
169
|
-
<SectionLabel>Globals</SectionLabel>
|
|
170
|
-
<TypeList
|
|
171
|
-
models={grouped.globals}
|
|
172
|
-
workspaceSlug={workspace.workspaceSlug}
|
|
173
|
-
defaultLocale={workspace.defaultLocale}
|
|
174
|
-
/>
|
|
175
|
-
</div>
|
|
176
|
-
) : null}
|
|
177
|
-
</>
|
|
178
|
-
)}
|
|
179
|
-
</nav>
|
|
180
|
-
<nav aria-label="Settings" {...stylex.props(styles.settings)}>
|
|
181
|
-
<SectionLabel>Settings</SectionLabel>
|
|
182
|
-
<ul {...stylex.props(styles.list)}>
|
|
183
|
-
<li>
|
|
184
|
-
<NavItem
|
|
185
|
-
to={settingsPath(workspace.workspaceSlug, "general")}
|
|
186
|
-
active={isWorkspaceSettingsPath(location.pathname)}
|
|
187
|
-
>
|
|
188
|
-
<GearIcon />
|
|
189
|
-
Settings
|
|
190
|
-
</NavItem>
|
|
191
|
-
</li>
|
|
192
|
-
{menu.map((item) => (
|
|
193
|
-
<li key={`${item.label}:${item.href}`}>
|
|
194
|
-
<NavAnchor href={item.href}>
|
|
195
|
-
<ExternalIcon />
|
|
196
|
-
{item.label}
|
|
197
|
-
</NavAnchor>
|
|
198
|
-
</li>
|
|
199
|
-
))}
|
|
200
|
-
</ul>
|
|
201
|
-
</nav>
|
|
202
|
-
</>
|
|
203
|
-
);
|
|
67
|
+
function SectionLabel({ children }: { children: React.ReactNode }) {
|
|
68
|
+
return <p {...stylex.props(styles.section)}>{children}</p>;
|
|
204
69
|
}
|
|
205
70
|
|
|
206
|
-
function SidebarFooter() {
|
|
71
|
+
function SidebarFooter({ compact = false }: { compact?: boolean }) {
|
|
207
72
|
return (
|
|
208
|
-
<div {...stylex.props(styles.footer)}>
|
|
209
|
-
<UserMenu />
|
|
73
|
+
<div {...stylex.props(styles.footer, compact && styles.footerCompact)}>
|
|
74
|
+
<UserMenu compact={compact} />
|
|
210
75
|
</div>
|
|
211
76
|
);
|
|
212
77
|
}
|
|
213
78
|
|
|
214
|
-
function SectionLabel({ children }: { children: React.ReactNode }) {
|
|
215
|
-
return <p {...stylex.props(styles.section)}>{children}</p>;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
79
|
const styles = stylex.create({
|
|
80
|
+
workspaceNav: {
|
|
81
|
+
display: "flex",
|
|
82
|
+
minHeight: 0,
|
|
83
|
+
flex: 1,
|
|
84
|
+
flexDirection: "column",
|
|
85
|
+
},
|
|
219
86
|
aside: {
|
|
220
87
|
display: "flex",
|
|
221
88
|
height: "100%",
|
|
222
|
-
width: "
|
|
89
|
+
width: "min(360px, 38vw)",
|
|
90
|
+
minWidth: "300px",
|
|
223
91
|
flexShrink: 0,
|
|
224
92
|
flexDirection: "column",
|
|
225
93
|
borderRightWidth: 1,
|
|
226
94
|
borderRightStyle: "solid",
|
|
227
95
|
borderRightColor: colors.border,
|
|
228
|
-
backgroundColor: colors.
|
|
96
|
+
backgroundColor: colors.bgSurface,
|
|
97
|
+
},
|
|
98
|
+
asideRail: {
|
|
99
|
+
width: "48px",
|
|
100
|
+
minWidth: "48px",
|
|
229
101
|
},
|
|
230
102
|
brand: {
|
|
231
103
|
flexShrink: 0,
|
|
@@ -302,4 +174,7 @@ const styles = stylex.create({
|
|
|
302
174
|
borderTopColor: colors.border,
|
|
303
175
|
padding: space.md,
|
|
304
176
|
},
|
|
177
|
+
footerCompact: {
|
|
178
|
+
padding: space.sm,
|
|
179
|
+
},
|
|
305
180
|
});
|