@backmatter/studio 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/chunks/{UiKitApp-ZZXQt3zy.js → UiKitApp-BDsrXN12.js} +323 -321
  2. package/dist/chunks/{WorkspaceRoutes-CCDPtLla.js → WorkspaceRoutes-BTZ1Ah4F.js} +33286 -33321
  3. package/dist/chunks/{dashboard-Dw0zfTXg.js → dashboard-FT7dm5wi.js} +2 -2
  4. package/dist/chunks/{index-DhZGCAru.js → index-DHJvh5Uy.js} +1 -1
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +51 -52
  7. package/dist/site-widget.js +9547 -10892
  8. package/package.json +2 -2
  9. package/src/api.ts +8 -26
  10. package/src/chat/ChangeCard.tsx +2 -10
  11. package/src/chat/ChatMessage.tsx +24 -19
  12. package/src/chat/ChatProvider.tsx +17 -15
  13. package/src/chat/DocListCard.tsx +2 -16
  14. package/src/chat/MentionPopover.tsx +8 -9
  15. package/src/chat/PointerChip.tsx +1 -1
  16. package/src/chat/chat-context.ts +7 -7
  17. package/src/chat/chat-helpers.ts +57 -14
  18. package/src/chrome/CommandPalette.tsx +2 -10
  19. package/src/chrome/ContentExplorerSidebar.tsx +51 -88
  20. package/src/chrome/EditorFocusContext.tsx +3 -3
  21. package/src/chrome/EditorStatusContext.tsx +0 -3
  22. package/src/chrome/WorkspaceNavContext.tsx +0 -2
  23. package/src/chrome/WorkspaceStatusBar.tsx +6 -23
  24. package/src/chrome/content-api-url.ts +3 -12
  25. package/src/chrome/content-repo-path.ts +8 -25
  26. package/src/chrome/content-type-groups.ts +0 -3
  27. package/src/chrome/folder-create-context.ts +15 -39
  28. package/src/pages/DocEditorPage.tsx +17 -33
  29. package/src/pages/EmbedChatPage.tsx +3 -3
  30. package/src/pages/EmbedPublishPage.tsx +2 -3
  31. package/src/pages/LegacyWorkspaceRedirect.tsx +2 -9
  32. package/src/pages/WorkspaceLayout.tsx +0 -5
  33. package/src/pages/preview-url.ts +6 -20
  34. package/src/pages/type-docs.ts +3 -6
  35. package/src/preview-frame.tsx +10 -4
  36. package/src/replica/store.ts +2 -10
  37. package/src/replica/types.ts +1 -2
  38. package/src/site-bar.tsx +3 -6
  39. package/src/site-dashboard-href.ts +1 -3
  40. package/src/site-widget.tsx +2 -3
  41. package/src/workspace-path.ts +3 -23
@@ -7,11 +7,28 @@ import type { ChatChangeSummary, ChatThread, DocStatus, ChatChangeStatus } from
7
7
  import type { DocRef } from "../chrome/EditorFocusContext";
8
8
 
9
9
  export type WriteToolOutput =
10
- | { ok: true; change: ChatChangeSummary }
10
+ | { ok: true; change?: ChatChangeSummary; changes?: ChatChangeSummary[] }
11
11
  | { ok: false; error: string; message: string };
12
12
 
13
13
  export function isWriteTool(name: string) {
14
- return name === "writeDoc" || name === "createDoc" || name === "renameDoc";
14
+ return (
15
+ name === "writeDoc" ||
16
+ name === "createDoc" ||
17
+ name === "renameDoc" ||
18
+ name === "deleteDoc" ||
19
+ name === "deleteFolder" ||
20
+ name === "moveFolder"
21
+ );
22
+ }
23
+
24
+ /** One write tool may record several files, as a folder move or delete does. */
25
+ export function changesInToolOutput(output: unknown): ChatChangeSummary[] {
26
+ if (!output || typeof output !== "object") return [];
27
+ const body = output as { ok?: boolean; change?: ChatChangeSummary; changes?: ChatChangeSummary[] };
28
+ if (body.ok === false) return [];
29
+ const many = Array.isArray(body.changes) ? body.changes.filter((change) => change?.id) : [];
30
+ if (many.length > 0) return many;
31
+ return body.change?.id ? [body.change] : [];
15
32
  }
16
33
 
17
34
  export function isCommitTool(name: string) {
@@ -22,7 +39,6 @@ const TREE_STATUSES = new Set<DocStatus>(["clean", "modified", "added", "conflic
22
39
 
23
40
  /** Statuses a tool result wants applied to the local doc list immediately. */
24
41
  export type TreeStatusPatch = {
25
- locale: string;
26
42
  type: string;
27
43
  slug: string;
28
44
  status: DocStatus;
@@ -38,8 +54,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
38
54
  if (!row || typeof row !== "object") continue;
39
55
  const item = row as Partial<TreeStatusPatch>;
40
56
  if (
41
- typeof item.locale !== "string" ||
42
- !item.locale ||
43
57
  typeof item.type !== "string" ||
44
58
  !item.type ||
45
59
  typeof item.slug !== "string" ||
@@ -50,7 +64,6 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
50
64
  continue;
51
65
  }
52
66
  patches.push({
53
- locale: item.locale,
54
67
  type: item.type,
55
68
  slug: item.slug,
56
69
  status: item.status as DocStatus,
@@ -60,6 +73,26 @@ export function treeStatusPatches(output: unknown): TreeStatusPatch[] {
60
73
  return patches;
61
74
  }
62
75
 
76
+ /** A deleted or absent row leaves the content tree. Other rows update status. */
77
+ export function detailForTreePatch(
78
+ workspaceId: string,
79
+ patch: TreeStatusPatch,
80
+ ): {
81
+ workspaceId: string;
82
+ type: string;
83
+ slug: string;
84
+ status?: TreeStatusPatch["status"];
85
+ remove?: boolean;
86
+ } {
87
+ const gone = patch.absent === true || patch.status === "deleted";
88
+ return {
89
+ workspaceId,
90
+ type: patch.type,
91
+ slug: patch.slug,
92
+ ...(gone ? { remove: true } : { status: patch.status }),
93
+ };
94
+ }
95
+
63
96
  export function toolLabel(name: string, input: unknown, output: unknown, state: string): string {
64
97
  const ref = (input as { docRef?: { type?: string; slug?: string } } | undefined)?.docRef;
65
98
  const doc = ref?.type && ref.slug ? `${ref.type}/${ref.slug}` : "";
@@ -84,6 +117,17 @@ export function toolLabel(name: string, input: unknown, output: unknown, state:
84
117
  return running ? `Creating ${doc}…` : `Created ${doc}`;
85
118
  case "renameDoc":
86
119
  return running ? `Renaming ${doc}…` : `Renamed ${doc}`;
120
+ case "deleteDoc":
121
+ return running ? `Deleting ${doc}…` : `Deleted ${doc}`;
122
+ case "deleteFolder": {
123
+ const path = (input as { path?: string } | undefined)?.path ?? "folder";
124
+ return running ? `Deleting ${path}…` : `Deleted ${path}`;
125
+ }
126
+ case "moveFolder": {
127
+ const from = (input as { from?: string } | undefined)?.from ?? "folder";
128
+ const to = (input as { to?: string } | undefined)?.to ?? "";
129
+ return running ? `Moving ${from}…` : `Moved ${from} to ${to}`;
130
+ }
87
131
  case "commit": {
88
132
  if (running) return "Publishing…";
89
133
  const published = output as { ok?: boolean; files?: unknown[] } | undefined;
@@ -221,15 +265,14 @@ export function changeIdsInMessage(message: UIMessage): string[] {
221
265
  if (!isToolUIPart(part)) continue;
222
266
  if (!isWriteTool(getToolName(part))) continue;
223
267
  if (part.state !== "output-available") continue;
224
- const out = part.output as WriteToolOutput | undefined;
225
- if (out?.ok) ids.push(out.change.id);
268
+ for (const change of changesInToolOutput(part.output)) ids.push(change.id);
226
269
  }
227
270
  return ids;
228
271
  }
229
272
 
230
273
  export type ChangeStackItem = Pick<
231
274
  ChatChangeSummary,
232
- "id" | "locale" | "type" | "slug" | "status" | "createdAt"
275
+ "id" | "type" | "slug" | "status" | "createdAt"
233
276
  >;
234
277
 
235
278
  function isLaterChange(a: ChangeStackItem, b: ChangeStackItem) {
@@ -247,10 +290,10 @@ export function presentedChangeStatus(
247
290
  all: ChangeStackItem[],
248
291
  ): ChatChangeStatus {
249
292
  if (change.status !== "applied") return change.status;
250
- const key = `${change.locale}/${change.type}/${change.slug}`;
293
+ const key = `${change.type}/${change.slug}`;
251
294
  const covered = all.some((other) => {
252
295
  if (other.id === change.id) return false;
253
- if (`${other.locale}/${other.type}/${other.slug}` !== key) return false;
296
+ if (`${other.type}/${other.slug}` !== key) return false;
254
297
  if (other.status !== "applied" && other.status !== "kept") return false;
255
298
  return isLaterChange(other, change);
256
299
  });
@@ -267,9 +310,9 @@ export function stackChanges(
267
310
  for (const part of message.parts) {
268
311
  if (!isToolUIPart(part) || part.state !== "output-available") continue;
269
312
  if (!isWriteTool(getToolName(part))) continue;
270
- const out = part.output as WriteToolOutput | undefined;
271
- if (!out?.ok) continue;
272
- byId.set(out.change.id, out.change);
313
+ for (const change of changesInToolOutput(part.output)) {
314
+ byId.set(change.id, change);
315
+ }
273
316
  }
274
317
  }
275
318
  for (const change of Object.values(overrides)) {
@@ -357,7 +357,7 @@ export function CommandPalette() {
357
357
  }
358
358
  for (const doc of docs) {
359
359
  list.push({
360
- id: `doc-${doc.locale}-${doc.type}-${doc.slug}`,
360
+ id: `doc-${doc.type}-${doc.slug}`,
361
361
  group: "Documents",
362
362
  icon: "document",
363
363
  label: `${doc.type}/${doc.slug}`,
@@ -370,15 +370,7 @@ export function CommandPalette() {
370
370
  : doc.status === "modified"
371
371
  ? "Modified"
372
372
  : doc.status,
373
- run: go(
374
- contentDocPath(
375
- workspaceNav.workspaceSlug,
376
- doc.type,
377
- doc.slug,
378
- doc.locale,
379
- workspaceNav.defaultLocale,
380
- ),
381
- ),
373
+ run: go(contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug)),
382
374
  });
383
375
  }
384
376
  }
@@ -3,7 +3,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
5
5
  import * as stylex from "@stylexjs/stylex";
6
- import { notifyTreeChanged, saveDoc, type DocListItem, type DocStatus } from "../api";
6
+ import {
7
+ notifyTreeChanged,
8
+ saveDoc,
9
+ type DocListItem,
10
+ type DocStatus,
11
+ type TreeChangeDetail,
12
+ } from "../api";
7
13
  import { useReplicaRevision, useReplicaState } from "../replica/useReplica";
8
14
  import { colors, radius, space, type } from "../styles/tokens.stylex";
9
15
  import { Tooltip } from "../ui/Tooltip";
@@ -121,10 +127,7 @@ function ContentExplorerSidebar({
121
127
  return map;
122
128
  }, [replica, replicaRevision, workspace.workspaceId]);
123
129
 
124
- const route = useMemo(
125
- () => parseContentRoute(location.pathname, searchParams.get("locale") ?? ""),
126
- [location.pathname, searchParams],
127
- );
130
+ const route = useMemo(() => parseContentRoute(location.pathname), [location.pathname]);
128
131
  const git = replica.git(workspace.workspaceId);
129
132
  const pendingCount =
130
133
  git?.changes.filter((c) => c.status !== "deleted").length ?? 0;
@@ -162,6 +165,28 @@ function ContentExplorerSidebar({
162
165
  });
163
166
  }, [workspace, route, models, docsByType]);
164
167
 
168
+ useEffect(() => {
169
+ function onTree(event: Event) {
170
+ const detail = (event as CustomEvent<TreeChangeDetail>).detail;
171
+ if (!detail?.type || !detail.slug || detail.remove) return;
172
+ if (
173
+ detail.workspaceId &&
174
+ detail.workspaceId !== workspace.workspaceId &&
175
+ detail.workspaceId !== workspace.workspaceSlug
176
+ ) {
177
+ return;
178
+ }
179
+ const model = models.find((item) => item.type === detail.type);
180
+ if (!model) return;
181
+ const repoPath = docRepoPath(model, { slug: detail.slug });
182
+ const parents = folderKeysOnPathTo(treePathFromRepo(repoPath));
183
+ const folder = parents[parents.length - 1];
184
+ if (folder) expandFolderPath(folder);
185
+ }
186
+ window.addEventListener("backmatter:tree-changed", onTree);
187
+ return () => window.removeEventListener("backmatter:tree-changed", onTree);
188
+ }, [models, workspace, expandFolderPath]);
189
+
165
190
  const empty = workspace.contentTypes.length === 0;
166
191
  const settingsActive = isWorkspaceSettingsPath(location.pathname);
167
192
  const treeOpen = useContentTreeOpen();
@@ -374,31 +399,21 @@ function ExplorerSection({
374
399
  const leaves: PathTreeFile[] = [];
375
400
  for (const model of models) {
376
401
  for (const doc of docsByType.get(model.type) ?? []) {
377
- const repoPath = docRepoPath(model, doc, workspace.defaultLocale);
402
+ if (doc.status === "deleted") continue;
403
+ const repoPath = docRepoPath(model, doc);
378
404
  const treePath = treePathFromRepo(repoPath);
379
405
  leaves.push({
380
406
  kind: "file",
381
407
  name: treePath.split("/").pop() ?? treePath,
382
408
  repoPath: treePath,
383
- href: contentDocPath(
384
- workspace.workspaceSlug,
385
- doc.type,
386
- doc.slug,
387
- doc.locale ?? "",
388
- workspace.defaultLocale,
389
- ),
390
- active: docMatchesRoute(route, doc, workspace.defaultLocale),
409
+ href: contentDocPath(workspace.workspaceSlug, doc.type, doc.slug),
410
+ active: docMatchesRoute(route, doc),
391
411
  dirty: isDirtyStatus(doc.status),
392
412
  });
393
413
  }
394
414
  }
395
415
  const built = buildPathTree(leaves);
396
- return withModelFolderPlaceholders(
397
- built,
398
- models,
399
- workspace.locales,
400
- workspace.defaultLocale,
401
- );
416
+ return withModelFolderPlaceholders(built, models);
402
417
  }, [models, docsByType, workspace, route]);
403
418
 
404
419
  const startNewFile = useCallback(
@@ -422,12 +437,7 @@ function ExplorerSection({
422
437
  return;
423
438
  }
424
439
  const list = docsByType.get(ctx.type) ?? [];
425
- const localeForDoc = ctx.locale || workspace.defaultLocale;
426
- const exists = list.some(
427
- (d) =>
428
- d.slug === slug &&
429
- (d.locale || workspace.defaultLocale) === localeForDoc,
430
- );
440
+ const exists = list.some((d) => d.slug === slug);
431
441
  if (exists) {
432
442
  setNewFileError("A file with that name already exists.");
433
443
  return;
@@ -436,30 +446,18 @@ function ExplorerSection({
436
446
  setNewFileError(null);
437
447
  try {
438
448
  const frontmatter = ctx.pageFormat ? { blocks: [] } : {};
439
- const { doc } = await saveDoc(
440
- workspace.workspaceId,
441
- ctx.locale,
442
- ctx.type,
443
- slug,
444
- { frontmatter, body: "" },
445
- );
449
+ const { doc } = await saveDoc(workspace.workspaceId, ctx.type, slug, {
450
+ frontmatter,
451
+ body: "",
452
+ });
446
453
  notifyTreeChanged({
447
454
  workspaceId: workspace.workspaceId,
448
- locale: doc.locale,
449
455
  type: doc.type,
450
456
  slug: doc.slug,
451
457
  status: doc.status,
452
458
  });
453
459
  setNewFileFolder(null);
454
- navigate(
455
- contentDocPath(
456
- workspace.workspaceSlug,
457
- doc.type,
458
- doc.slug,
459
- doc.locale ?? "",
460
- workspace.defaultLocale,
461
- ),
462
- );
460
+ navigate(contentDocPath(workspace.workspaceSlug, doc.type, doc.slug));
463
461
  } catch (err) {
464
462
  setNewFileError(err instanceof Error ? err.message : "Could not create file.");
465
463
  } finally {
@@ -483,8 +481,6 @@ function ExplorerSection({
483
481
  expanded={expanded}
484
482
  onToggleFolder={onToggleFolder}
485
483
  models={models}
486
- locales={workspace.locales}
487
- defaultLocale={workspace.defaultLocale}
488
484
  canWrite={canWrite}
489
485
  newFileFolder={newFileFolder}
490
486
  newFileError={newFileError}
@@ -507,8 +503,6 @@ function PathTree({
507
503
  expanded,
508
504
  onToggleFolder,
509
505
  models,
510
- locales,
511
- defaultLocale,
512
506
  canWrite,
513
507
  newFileFolder,
514
508
  newFileError,
@@ -522,8 +516,6 @@ function PathTree({
522
516
  expanded: Set<string>;
523
517
  onToggleFolder: (pathKey: string) => void;
524
518
  models: ContentTypeNav[];
525
- locales: string[];
526
- defaultLocale: string;
527
519
  canWrite: boolean;
528
520
  newFileFolder: string | null;
529
521
  newFileError: string | null;
@@ -548,12 +540,7 @@ function PathTree({
548
540
  onToggle={() => onToggleFolder(node.pathKey)}
549
541
  createContext={
550
542
  canWrite
551
- ? resolveFolderCreateContext(
552
- node.pathKey,
553
- models,
554
- locales,
555
- defaultLocale,
556
- )
543
+ ? resolveFolderCreateContext(node.pathKey, models)
557
544
  : null
558
545
  }
559
546
  onNewFile={
@@ -566,23 +553,13 @@ function PathTree({
566
553
  <NewFileTreeRow
567
554
  depth={depth + 1}
568
555
  ext={
569
- resolveFolderCreateContext(
570
- node.pathKey,
571
- models,
572
- locales,
573
- defaultLocale,
574
- )?.ext ?? "mdx"
556
+ resolveFolderCreateContext(node.pathKey, models)?.ext ?? "mdx"
575
557
  }
576
558
  error={newFileError}
577
559
  disabled={creating}
578
560
  onCancel={onCancelNewFile}
579
561
  onSubmit={(name) => {
580
- const ctx = resolveFolderCreateContext(
581
- node.pathKey,
582
- models,
583
- locales,
584
- defaultLocale,
585
- );
562
+ const ctx = resolveFolderCreateContext(node.pathKey, models);
586
563
  if (ctx) onSubmitNewFile(node.pathKey, ctx, name);
587
564
  }}
588
565
  />
@@ -594,8 +571,6 @@ function PathTree({
594
571
  expanded={expanded}
595
572
  onToggleFolder={onToggleFolder}
596
573
  models={models}
597
- locales={locales}
598
- defaultLocale={defaultLocale}
599
574
  canWrite={canWrite}
600
575
  newFileFolder={newFileFolder}
601
576
  newFileError={newFileError}
@@ -807,11 +782,9 @@ function findActiveRepoPath(
807
782
  if (!route.type || !route.docSlug) return null;
808
783
  const model = models.find((m) => m.type === route.type);
809
784
  if (!model) return null;
810
- const doc = (docsByType.get(route.type) ?? []).find((d) =>
811
- docMatchesRoute(route, d, workspace.defaultLocale),
812
- );
785
+ const doc = (docsByType.get(route.type) ?? []).find((d) => docMatchesRoute(route, d));
813
786
  if (!doc) return null;
814
- return docRepoPath(model, doc, workspace.defaultLocale);
787
+ return docRepoPath(model, doc);
815
788
  }
816
789
 
817
790
  function IconButton({
@@ -840,28 +813,18 @@ function IconButton({
840
813
  type ContentRoute = {
841
814
  type: string | null;
842
815
  docSlug: string | null;
843
- locale: string;
844
816
  };
845
817
 
846
- function parseContentRoute(pathname: string, localeParam: string): ContentRoute {
818
+ function parseContentRoute(pathname: string): ContentRoute {
847
819
  const match = pathname.match(/\/content\/([^/]+)(?:\/([^/?]+))?/);
848
- if (!match) return { type: null, docSlug: null, locale: "" };
820
+ if (!match) return { type: null, docSlug: null };
849
821
  const type = decodeURIComponent(match[1]!);
850
822
  const docSlug = match[2] ? decodeURIComponent(match[2]) : null;
851
- return { type, docSlug, locale: localeParam };
823
+ return { type, docSlug };
852
824
  }
853
825
 
854
- function docMatchesRoute(
855
- route: ContentRoute,
856
- doc: DocListItem,
857
- defaultLocale: string,
858
- ) {
859
- if (!route.type || route.docSlug !== doc.slug || route.type !== doc.type) {
860
- return false;
861
- }
862
- const docLocale = doc.locale || defaultLocale;
863
- const urlLocale = route.locale || defaultLocale;
864
- return docLocale === urlLocale;
826
+ function docMatchesRoute(route: ContentRoute, doc: DocListItem) {
827
+ return Boolean(route.type && route.docSlug === doc.slug && route.type === doc.type);
865
828
  }
866
829
 
867
830
  function isDirtyStatus(status: DocStatus) {
@@ -11,7 +11,7 @@ import {
11
11
  useState,
12
12
  } from "react";
13
13
 
14
- export type DocRef = { locale: string; type: string; slug: string };
14
+ export type DocRef = { type: string; slug: string };
15
15
 
16
16
  export type EditorSelection = { start: number; end: number };
17
17
 
@@ -23,12 +23,12 @@ export type EditorFocus = {
23
23
  };
24
24
 
25
25
  export function docKey(ref: DocRef): string {
26
- return `${ref.locale}/${ref.type}/${ref.slug}`;
26
+ return `${ref.type}/${ref.slug}`;
27
27
  }
28
28
 
29
29
  export function sameDocRef(a: DocRef | null | undefined, b: DocRef | null | undefined) {
30
30
  if (!a || !b) return false;
31
- return a.locale === b.locale && a.type === b.type && a.slug === b.slug;
31
+ return a.type === b.type && a.slug === b.slug;
32
32
  }
33
33
 
34
34
  type EditorFocusContextValue = {
@@ -8,7 +8,6 @@ import {
8
8
  } from "react";
9
9
 
10
10
  export type EditorStatus = {
11
- locale: string | null;
12
11
  /** committed | uncommitted | conflict */
13
12
  status: string | null;
14
13
  saveLabel: string | null;
@@ -16,7 +15,6 @@ export type EditorStatus = {
16
15
  };
17
16
 
18
17
  const EMPTY: EditorStatus = {
19
- locale: null,
20
18
  status: null,
21
19
  saveLabel: null,
22
20
  saveTone: "muted",
@@ -38,7 +36,6 @@ const EditorStatusContext = createContext<EditorStatusContextValue | null>(
38
36
 
39
37
  function sameStatus(a: EditorStatus, b: EditorStatus) {
40
38
  return (
41
- a.locale === b.locale &&
42
39
  a.status === b.status &&
43
40
  a.saveLabel === b.saveLabel &&
44
41
  a.saveTone === b.saveTone
@@ -13,8 +13,6 @@ export type WorkspaceNav = {
13
13
  workspaceSlug: string;
14
14
  workspaceName: string;
15
15
  role: string;
16
- defaultLocale: string;
17
- locales: string[];
18
16
  contentTypes: string[];
19
17
  contentModels: ContentTypeNav[];
20
18
  siteUrls: string[];
@@ -56,12 +56,11 @@ export const STATUS_LABEL: Record<ChangeStatus, string> = {
56
56
  };
57
57
 
58
58
  function changeKey(doc: GitChange) {
59
- return `${doc.locale}/${doc.type}/${doc.slug}`;
59
+ return `${doc.type}/${doc.slug}`;
60
60
  }
61
61
 
62
62
  function ConflictResolvePanel({
63
63
  workspaceSlug,
64
- defaultLocale,
65
64
  conflicts,
66
65
  resolvingKey,
67
66
  error,
@@ -69,7 +68,6 @@ function ConflictResolvePanel({
69
68
  onResolve,
70
69
  }: {
71
70
  workspaceSlug: string | null;
72
- defaultLocale: string;
73
71
  conflicts: GitChange[];
74
72
  resolvingKey: string | null;
75
73
  error: string | null;
@@ -88,13 +86,7 @@ function ConflictResolvePanel({
88
86
  {conflicts.map((doc) => {
89
87
  const key = changeKey(doc);
90
88
  const href = workspaceSlug
91
- ? contentDocPath(
92
- workspaceSlug,
93
- doc.type,
94
- doc.slug,
95
- doc.locale,
96
- defaultLocale,
97
- )
89
+ ? contentDocPath(workspaceSlug, doc.type, doc.slug)
98
90
  : null;
99
91
  return (
100
92
  <li key={key} {...stylex.props(styles.conflictRow)}>
@@ -107,7 +99,6 @@ function ConflictResolvePanel({
107
99
  <span {...stylex.props(styles.changeName)}>
108
100
  {doc.type}/{doc.slug}
109
101
  </span>
110
- <span {...stylex.props(popup.itemMeta)}>{doc.locale}</span>
111
102
  </Link>
112
103
  ) : (
113
104
  <span {...stylex.props(styles.changeName)}>
@@ -293,10 +284,9 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
293
284
  setResolvingKey(key);
294
285
  setError(null);
295
286
  try {
296
- await resolveDoc(workspaceId, doc.locale, doc.type, doc.slug, strategy);
287
+ await resolveDoc(workspaceId, doc.type, doc.slug, strategy);
297
288
  notifyTreeChanged({
298
289
  workspaceId,
299
- locale: doc.locale,
300
290
  type: doc.type,
301
291
  slug: doc.slug,
302
292
  status: strategy === "theirs" ? "clean" : "modified",
@@ -341,13 +331,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
341
331
  const slug = ref.slice(slash + 1);
342
332
  const doc = changes.find((change) => change.type === type && change.slug === slug);
343
333
  if (!doc) return null;
344
- return contentDocPath(
345
- workspaceNav.workspaceSlug,
346
- doc.type,
347
- doc.slug,
348
- doc.locale,
349
- workspaceNav.defaultLocale,
350
- );
334
+ return contentDocPath(workspaceNav.workspaceSlug, doc.type, doc.slug);
351
335
  }
352
336
 
353
337
  const commitLabel = committing
@@ -444,7 +428,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
444
428
  <div {...stylex.props(styles.changes)}>
445
429
  <p {...stylex.props(popup.groupLabel)}>Uncommitted</p>
446
430
  {changes.map((doc) => (
447
- <ChangeRow key={`${doc.locale}/${doc.type}/${doc.slug}`} doc={doc} />
431
+ <ChangeRow key={changeKey(doc)} doc={doc} />
448
432
  ))}
449
433
  </div>
450
434
  </>
@@ -566,7 +550,6 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
566
550
  {action === "resolve" ? (
567
551
  <ConflictResolvePanel
568
552
  workspaceSlug={workspaceNav?.workspaceSlug ?? null}
569
- defaultLocale={workspaceNav?.defaultLocale ?? "en"}
570
553
  conflicts={conflicts}
571
554
  resolvingKey={resolvingKey}
572
555
  error={error}
@@ -598,7 +581,7 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
598
581
  ) : null}
599
582
  <ul {...stylex.props(styles.commitFiles)}>
600
583
  {changes.map((doc) => (
601
- <li key={`${doc.locale}/${doc.type}/${doc.slug}`}>
584
+ <li key={changeKey(doc)}>
602
585
  <ChangeRow
603
586
  doc={doc}
604
587
  invalid={
@@ -1,7 +1,7 @@
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 = { locale: string; type: string; slug: string };
4
+ export type ContentApiDoc = { type: string; slug: string };
5
5
 
6
6
  export type ContentApiContext = {
7
7
  doc: ContentApiDoc | null;
@@ -10,16 +10,12 @@ export type ContentApiContext = {
10
10
 
11
11
  export function contentApiContextFromPath(
12
12
  pathname: string,
13
- search = "",
13
+ _search = "",
14
14
  ): ContentApiContext {
15
- const localeFromQuery = new URLSearchParams(
16
- search.startsWith("?") ? search.slice(1) : search,
17
- ).get("locale");
18
15
  const doc = pathname.match(/\/content\/([^/]+)\/([^/]+)\/?$/);
19
16
  if (doc) {
20
17
  return {
21
18
  doc: {
22
- locale: localeFromQuery || "en",
23
19
  type: decodeURIComponent(doc[1]!),
24
20
  slug: decodeURIComponent(doc[2]!),
25
21
  },
@@ -34,7 +30,6 @@ export function contentApiContextFromPath(
34
30
  if (legacyDoc) {
35
31
  return {
36
32
  doc: {
37
- locale: decodeURIComponent(legacyDoc[1]!),
38
33
  type: decodeURIComponent(legacyDoc[2]!),
39
34
  slug: decodeURIComponent(legacyDoc[3]!),
40
35
  },
@@ -53,15 +48,12 @@ export function publicContentApiUrl(input: {
53
48
  workspaceId: string;
54
49
  branch?: string | null;
55
50
  defaultBranch?: string | null;
56
- locale?: string;
57
51
  type?: string;
58
52
  slug?: string;
59
53
  }): string {
60
54
  const origin = input.apiUrl.replace(/\/$/, "");
61
55
  const path =
62
- input.locale && input.type && input.slug
63
- ? `/${input.locale}/${input.type}/${input.slug}`
64
- : "";
56
+ input.type && input.slug ? `/${input.type}/${input.slug}` : "";
65
57
  if (!input.workspaceId.startsWith("ws_")) {
66
58
  throw new Error("public content URLs must use the workspace id (ws_…), not the slug");
67
59
  }
@@ -94,7 +86,6 @@ export function contentApiLinks(input: {
94
86
  label: "This document",
95
87
  href: publicContentApiUrl({
96
88
  ...base,
97
- locale: ctx.doc.locale,
98
89
  type: ctx.doc.type,
99
90
  slug: ctx.doc.slug,
100
91
  }),