@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
@@ -1,28 +1,17 @@
1
1
  // packages/studio/src/chrome/content-repo-path.ts
2
2
  import { IMPLICIT_SINGLETON_SLUG, repoPathFromModel } from "@backmatter/core";
3
3
  import type { DocListItem } from "../api";
4
- import { modelHasLocale, type ContentTypeNav } from "./content-type-groups";
4
+ import type { ContentTypeNav } from "./content-type-groups";
5
5
 
6
6
  export function docRepoPath(
7
7
  model: ContentTypeNav,
8
- doc: Pick<DocListItem, "locale" | "slug">,
9
- defaultLocale: string,
8
+ doc: Pick<DocListItem, "slug">,
10
9
  ): string {
11
- const locale = modelHasLocale(model.path)
12
- ? doc.locale || defaultLocale
13
- : undefined;
14
- return repoPathFromModel(model, locale, doc.slug);
10
+ return repoPathFromModel(model, doc.slug);
15
11
  }
16
12
 
17
- export function singletonRepoPath(
18
- model: ContentTypeNav,
19
- defaultLocale: string,
20
- ): string {
21
- return docRepoPath(
22
- model,
23
- { slug: IMPLICIT_SINGLETON_SLUG, locale: defaultLocale },
24
- defaultLocale,
25
- );
13
+ export function singletonRepoPath(model: ContentTypeNav): string {
14
+ return docRepoPath(model, { slug: IMPLICIT_SINGLETON_SLUG });
26
15
  }
27
16
 
28
17
  export function repoPathBasename(repoPath: string): string {
@@ -39,14 +28,8 @@ export function fileExtFromRepoPath(repoPath: string): string {
39
28
  /** Label for a row in the content tree (repo-relative filename). */
40
29
  export function docTreeLabel(
41
30
  model: ContentTypeNav,
42
- doc: Pick<DocListItem, "locale" | "slug">,
43
- defaultLocale: string,
31
+ doc: Pick<DocListItem, "slug">,
44
32
  ): string {
45
- const repoPath = docRepoPath(model, doc, defaultLocale);
46
- const name = repoPathBasename(repoPath);
47
- const locale = doc.locale || defaultLocale;
48
- if (modelHasLocale(model.path) && locale !== defaultLocale) {
49
- return `${locale}/${name}`;
50
- }
51
- return name;
33
+ const repoPath = docRepoPath(model, doc);
34
+ return repoPathBasename(repoPath);
52
35
  }
@@ -27,6 +27,3 @@ export function groupContentTypes(models: ContentTypeNav[]) {
27
27
  return { pages, data, globals };
28
28
  }
29
29
 
30
- export function modelHasLocale(path: string) {
31
- return path.includes("{locale}");
32
- }
@@ -1,7 +1,6 @@
1
1
  // packages/studio/src/chrome/folder-create-context.ts
2
2
  import { contentExtFromPath } from "@backmatter/core";
3
3
  import type { ContentTypeNav } from "./content-type-groups";
4
- import { modelHasLocale } from "./content-type-groups";
5
4
  import { treePathFromRepo } from "./content-path-tree";
6
5
  import type { PathTreeFolder, PathTreeNode } from "./content-path-tree";
7
6
  import { docRepoPath, repoPathBasename } from "./content-repo-path";
@@ -10,7 +9,6 @@ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10
9
 
11
10
  export type FolderCreateContext = {
12
11
  type: string;
13
- locale: string;
14
12
  ext: string;
15
13
  /** JSON landing pages need `blocks: []` in frontmatter on create. */
16
14
  pageFormat: boolean;
@@ -35,19 +33,8 @@ export function slugFromNewFileName(name: string, expectedExt: string): string {
35
33
  return base;
36
34
  }
37
35
 
38
- export function folderPathKeyForModel(
39
- model: ContentTypeNav,
40
- locale: string | undefined,
41
- defaultLocale: string,
42
- ): string {
43
- const loc = modelHasLocale(model.path)
44
- ? locale || defaultLocale
45
- : undefined;
46
- const repo = docRepoPath(
47
- model,
48
- { slug: "new", locale: loc ?? "" },
49
- defaultLocale,
50
- );
36
+ export function folderPathKeyForModel(model: ContentTypeNav): string {
37
+ const repo = docRepoPath(model, { slug: "new" });
51
38
  const tree = treePathFromRepo(repo);
52
39
  const parts = tree.split("/").filter(Boolean);
53
40
  parts.pop();
@@ -57,26 +44,15 @@ export function folderPathKeyForModel(
57
44
  export function resolveFolderCreateContext(
58
45
  pathKey: string,
59
46
  models: ContentTypeNav[],
60
- locales: string[],
61
- defaultLocale: string,
62
47
  ): FolderCreateContext | null {
63
48
  const matches: FolderCreateContext[] = [];
64
49
  for (const model of models) {
65
50
  if (model.singleton) continue;
66
51
  const ext = contentExtFromPath(model.path);
67
52
  const pageFormat = ext === "json";
68
- if (modelHasLocale(model.path)) {
69
- for (const locale of locales) {
70
- const key = folderPathKeyForModel(model, locale, defaultLocale);
71
- if (key === pathKey) {
72
- matches.push({ type: model.type, locale, ext, pageFormat });
73
- }
74
- }
75
- } else {
76
- const key = folderPathKeyForModel(model, undefined, defaultLocale);
77
- if (key === pathKey) {
78
- matches.push({ type: model.type, locale: "", ext, pageFormat });
79
- }
53
+ const key = folderPathKeyForModel(model);
54
+ if (key === pathKey) {
55
+ matches.push({ type: model.type, ext, pageFormat });
80
56
  }
81
57
  }
82
58
  if (matches.length !== 1) return null;
@@ -119,23 +95,23 @@ function insertFolderPath(nodes: PathTreeNode[], segments: string[], pathKey: st
119
95
  return next;
120
96
  }
121
97
 
122
- /** Ensure each model's content directory exists as a folder (even with no files yet). */
98
+ function treeHasFile(nodes: PathTreeNode[]): boolean {
99
+ for (const node of nodes) {
100
+ if (node.kind === "file") return true;
101
+ if (treeHasFile(node.children)) return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /** Empty type folders so the first file in a model still has a folder row. */
123
107
  export function withModelFolderPlaceholders(
124
108
  tree: PathTreeNode[],
125
109
  models: ContentTypeNav[],
126
- locales: string[],
127
- defaultLocale: string,
128
110
  ): PathTreeNode[] {
129
111
  const keys = new Set<string>();
130
112
  for (const model of models) {
131
113
  if (model.singleton) continue;
132
- if (modelHasLocale(model.path)) {
133
- for (const locale of locales) {
134
- keys.add(folderPathKeyForModel(model, locale, defaultLocale));
135
- }
136
- } else {
137
- keys.add(folderPathKeyForModel(model, undefined, defaultLocale));
138
- }
114
+ keys.add(folderPathKeyForModel(model));
139
115
  }
140
116
  let out = tree;
141
117
  for (const key of [...keys].sort(compareName)) {
@@ -60,7 +60,7 @@ import { BodyField } from "./BodyProse";
60
60
  import { BlocksList } from "./BlocksList";
61
61
  import { blocksFromFrontmatter } from "./blocks-list";
62
62
  import type { WorkspaceOutletContext } from "../workspace/types";
63
- import { contentDocPath, defaultLocaleOf, settingsPath } from "../workspace-path";
63
+ import { contentDocPath, settingsPath } from "../workspace-path";
64
64
  import { bindPreviewEditor, previewFrameTransport } from "./preview-bridge";
65
65
  import { editorGrabAction } from "./editor-grab";
66
66
  import { useChatUI } from "../chat/ChatProvider";
@@ -190,10 +190,6 @@ export function DocEditorPage() {
190
190
  const model = models.find((m) => m.type === type);
191
191
  const embed = searchParams.get("embed") === "1";
192
192
  const showPreview = !embed && model?.route !== false;
193
- const locale = model?.path.includes("{locale}")
194
- ? searchParams.get("locale") || defaultLocaleOf(workspace.locales)
195
- : "";
196
-
197
193
  const [source, setSource] = useState<ContentSource | null>(null);
198
194
  const [docStatus, setDocStatus] = useState<DocStatus | null>(null);
199
195
  const [branch, setBranch] = useState<string | null>(null);
@@ -233,7 +229,7 @@ export function DocEditorPage() {
233
229
  useSetEditorStatus();
234
230
  const editorActions = useSetEditorActions();
235
231
  const { setFocus: setEditorFocus, aiEditingDocKey } = useEditorFocus();
236
- const docRef = useMemo(() => ({ locale, type, slug }), [locale, type, slug]);
232
+ const docRef = useMemo(() => ({ type, slug }), [type, slug]);
237
233
  const aiEditing = aiEditingDocKey !== null && aiEditingDocKey === docKey(docRef);
238
234
  const aiEditingRef = useRef(aiEditing);
239
235
  aiEditingRef.current = aiEditing;
@@ -250,9 +246,8 @@ export function DocEditorPage() {
250
246
  // Mirrors docStatus for event handlers that must not re-subscribe on change.
251
247
  const docStatusRef = useRef<DocStatus | null>(null);
252
248
  docStatusRef.current = docStatus;
253
- const channelRef = useRef<PreviewChannelRef>({ locale, type, slug });
249
+ const channelRef = useRef<PreviewChannelRef>({ type, slug });
254
250
  channelRef.current = {
255
- locale,
256
251
  type,
257
252
  slug,
258
253
  ...(branch ? { branch } : {}),
@@ -269,11 +264,10 @@ export function DocEditorPage() {
269
264
  const blocksFieldRef = useRef(blocksField);
270
265
  blocksFieldRef.current = blocksField;
271
266
  const siteUrl = primarySiteUrl(workspace.siteUrls ?? []);
272
- const liveUrl = productionDocUrl(siteUrl, modelForType(models, type), locale, slug);
267
+ const liveUrl = productionDocUrl(siteUrl, modelForType(models, type), slug);
273
268
  const previewPath = editorPreviewPath(
274
269
  modelForType(models, type),
275
270
  slug,
276
- locale,
277
271
  branch,
278
272
  tree,
279
273
  );
@@ -340,7 +334,6 @@ export function DocEditorPage() {
340
334
  });
341
335
  if (decision.action === "add") {
342
336
  addPointer({
343
- locale: channelRef.current.locale,
344
337
  type: channelRef.current.type,
345
338
  slug: channelRef.current.slug,
346
339
  blockId: decision.blockId,
@@ -545,7 +538,7 @@ export function DocEditorPage() {
545
538
 
546
539
  async function reload() {
547
540
  try {
548
- const data = await fetchDoc(workspaceId, locale, type, slug);
541
+ const data = await fetchDoc(workspaceId, type, slug);
549
542
  hydrated.current = false;
550
543
  setSource(data.doc.source);
551
544
  setDocStatus(data.doc.status);
@@ -588,7 +581,7 @@ export function DocEditorPage() {
588
581
  hydrated.current = true;
589
582
  });
590
583
  try {
591
- const res = await saveDoc(workspaceId, locale, type, slug, {
584
+ const res = await saveDoc(workspaceId, type, slug, {
592
585
  frontmatter: next,
593
586
  body: "",
594
587
  });
@@ -596,7 +589,6 @@ export function DocEditorPage() {
596
589
  setSource("tree");
597
590
  notifyTreeChanged({
598
591
  workspaceId,
599
- locale,
600
592
  type,
601
593
  slug,
602
594
  status: res.doc.status,
@@ -623,7 +615,7 @@ export function DocEditorPage() {
623
615
  }
624
616
  function onDocChanged(e: Event) {
625
617
  const detail = (e as CustomEvent<DocChangedDetail>).detail;
626
- if (!sameDocRef(detail?.docRef, { locale, type, slug })) return;
618
+ if (!sameDocRef(detail?.docRef, { type, slug })) return;
627
619
  onChanged();
628
620
  }
629
621
  // Chat publishes status with the tool result. Flip the badge before the reload returns.
@@ -632,7 +624,7 @@ export function DocEditorPage() {
632
624
  if (!detail?.status || detail.remove) return;
633
625
  const ws = detail.workspaceId;
634
626
  if (ws && ws !== workspace.id && ws !== workspace.slug) return;
635
- if (detail.locale !== locale || detail.type !== type || detail.slug !== slug) return;
627
+ if (detail.type !== type || detail.slug !== slug) return;
636
628
  docStatusRef.current = detail.status;
637
629
  setDocStatus(detail.status);
638
630
  setSource(detail.status === "clean" ? "head" : "tree");
@@ -645,7 +637,7 @@ export function DocEditorPage() {
645
637
  onChanged();
646
638
  return;
647
639
  }
648
- void fetchDoc(workspaceId, locale, type, slug)
640
+ void fetchDoc(workspaceId, type, slug)
649
641
  .then((data) => setDocStatus(data.doc.status))
650
642
  .catch(() => {});
651
643
  }
@@ -660,7 +652,7 @@ export function DocEditorPage() {
660
652
  window.removeEventListener(DOC_CHANGED_EVENT, onDocChanged);
661
653
  };
662
654
  // eslint-disable-next-line react-hooks/exhaustive-deps
663
- }, [workspaceId, workspace.slug, locale, type, slug]);
655
+ }, [workspaceId, workspace.slug, type, slug]);
664
656
 
665
657
  // Publish the focused document to the global AI chat (debounced).
666
658
  useEffect(() => {
@@ -698,14 +690,13 @@ export function DocEditorPage() {
698
690
  setStatus(null);
699
691
  }
700
692
  try {
701
- const res = await saveDoc(workspaceId, locale, type, slug, payload);
693
+ const res = await saveDoc(workspaceId, type, slug, payload);
702
694
  if (gen !== saveGen.current) return;
703
695
  setSource("tree");
704
696
  setDocStatus(res.doc.status);
705
697
  setSaveState("saved");
706
698
  notifyTreeChanged({
707
699
  workspaceId,
708
- locale,
709
700
  type,
710
701
  slug,
711
702
  status: res.doc.status,
@@ -747,9 +738,9 @@ export function DocEditorPage() {
747
738
  setStatus(null);
748
739
  saveGen.current += 1;
749
740
  try {
750
- await revertDoc(workspaceId, locale, type, slug);
741
+ await revertDoc(workspaceId, type, slug);
751
742
  setStatus("Reverted to the committed version");
752
- notifyTreeChanged({ workspaceId, locale, type, slug, status: "clean" });
743
+ notifyTreeChanged({ workspaceId, type, slug, status: "clean" });
753
744
  await reload();
754
745
  remountPreview();
755
746
  } catch (err) {
@@ -766,7 +757,7 @@ export function DocEditorPage() {
766
757
  setStatus(null);
767
758
  saveGen.current += 1;
768
759
  try {
769
- await resolveDoc(workspaceId, locale, type, slug, strategy);
760
+ await resolveDoc(workspaceId, type, slug, strategy);
770
761
  notifyTreeChanged();
771
762
  if (strategy === "theirs") {
772
763
  setStatus("Took the committed version");
@@ -774,7 +765,7 @@ export function DocEditorPage() {
774
765
  remountPreview();
775
766
  } else {
776
767
  setStatus("Kept your version; it will overwrite theirs on commit");
777
- const data = await fetchDoc(workspaceId, locale, type, slug);
768
+ const data = await fetchDoc(workspaceId, type, slug);
778
769
  setDocStatus(data.doc.status);
779
770
  }
780
771
  } catch (err) {
@@ -806,12 +797,11 @@ export function DocEditorPage() {
806
797
  const statusLabel = docStatusLabel(docStatus);
807
798
  useEffect(() => {
808
799
  setEditorStatus({
809
- locale,
810
800
  status: statusLabel,
811
801
  saveLabel,
812
802
  saveTone,
813
803
  });
814
- }, [setEditorStatus, locale, saveLabel, saveTone, statusLabel]);
804
+ }, [setEditorStatus, saveLabel, saveTone, statusLabel]);
815
805
 
816
806
  const hasTreeCopy = source === "tree";
817
807
  useEffect(() => {
@@ -903,13 +893,7 @@ export function DocEditorPage() {
903
893
  {...stylex.props(styles.editPage)}
904
894
  onClick={() => {
905
895
  navigate(
906
- contentDocPath(
907
- workspace.slug,
908
- previewOffer.type,
909
- previewOffer.slug,
910
- previewOffer.locale,
911
- defaultLocaleOf(workspace.locales),
912
- ),
896
+ contentDocPath(workspace.slug, previewOffer.type, previewOffer.slug),
913
897
  );
914
898
  }}
915
899
  >
@@ -52,9 +52,9 @@ function FocusBridge() {
52
52
  const data = event.data as { type?: string; doc?: Partial<DocRef> };
53
53
  if (data?.type !== "backmatter:focus") return;
54
54
  const doc = data.doc;
55
- if (!doc?.locale || !doc.type || !doc.slug) return;
55
+ if (!doc?.type || !doc.slug) return;
56
56
  setFocus({
57
- docRef: { locale: doc.locale, type: doc.type, slug: doc.slug },
57
+ docRef: { type: doc.type, slug: doc.slug },
58
58
  frontmatter: {},
59
59
  body: "",
60
60
  selection: null,
@@ -89,7 +89,7 @@ function PointerBridge() {
89
89
  const data = event.data as { type?: string; grab?: unknown; doc?: unknown };
90
90
  if (data?.type !== "backmatter:pointer-grab") return;
91
91
  const pointer = chatPointerFromGrab(
92
- (data.doc ?? {}) as { locale?: unknown; type?: unknown; slug?: unknown },
92
+ (data.doc ?? {}) as { type?: unknown; slug?: unknown },
93
93
  data.grab,
94
94
  );
95
95
  if (pointer) addPointer(pointer);
@@ -39,7 +39,7 @@ function parentOrigin(): string {
39
39
  }
40
40
 
41
41
  function changeKey(doc: GitChange) {
42
- return `${doc.locale}/${doc.type}/${doc.slug}`;
42
+ return `${doc.type}/${doc.slug}`;
43
43
  }
44
44
 
45
45
  const STATUS_LABEL: Record<ChangeStatus, string> = {
@@ -129,10 +129,9 @@ export function EmbedPublishPage({
129
129
  setResolvingKey(key);
130
130
  setError(null);
131
131
  try {
132
- await resolveDoc(workspaceId, doc.locale, doc.type, doc.slug, strategy);
132
+ await resolveDoc(workspaceId, doc.type, doc.slug, strategy);
133
133
  notifyTreeChanged({
134
134
  workspaceId,
135
- locale: doc.locale,
136
135
  type: doc.type,
137
136
  slug: doc.slug,
138
137
  status: strategy === "theirs" ? "clean" : "modified",
@@ -4,10 +4,7 @@ import { Navigate, useLocation, useParams } from "react-router";
4
4
  import { fetchWorkspace } from "../api";
5
5
  import { PageFrame } from "../ui/Page";
6
6
  import { Text } from "../ui/Text";
7
- import {
8
- defaultLocaleOf,
9
- rewriteLegacyWorkspacePath,
10
- } from "../workspace-path";
7
+ import { rewriteLegacyWorkspacePath } from "../workspace-path";
11
8
 
12
9
  export function LegacyWorkspaceRedirect() {
13
10
  const { workspaceId = "", "*": rest } = useParams();
@@ -26,11 +23,7 @@ export function LegacyWorkspaceRedirect() {
26
23
  const pathname = rest
27
24
  ? `/workspaces/${workspaceId}/${rest}`
28
25
  : `/workspaces/${workspaceId}`;
29
- const rewritten = rewriteLegacyWorkspacePath(
30
- pathname,
31
- workspace.slug,
32
- defaultLocaleOf(workspace.locales),
33
- );
26
+ const rewritten = rewriteLegacyWorkspacePath(pathname, workspace.slug);
34
27
  const url = new URL(rewritten, "http://local");
35
28
  new URLSearchParams(location.search).forEach((value, key) => {
36
29
  url.searchParams.set(key, value);
@@ -12,7 +12,6 @@ import { PageFrame } from "../ui/Page";
12
12
  import { Text } from "../ui/Text";
13
13
  import type { WorkspaceOutletContext } from "../workspace/types";
14
14
  import {
15
- defaultLocaleOf,
16
15
  isWorkspaceEditorPath,
17
16
  isWorkspaceMediaPath,
18
17
  isWorkspaceSettingsPath,
@@ -65,10 +64,6 @@ export function WorkspaceLayout() {
65
64
  workspaceSlug: workspace.slug,
66
65
  workspaceName: workspace.name,
67
66
  role: workspace.role,
68
- defaultLocale: defaultLocaleOf(workspace.locales),
69
- locales: workspace.locales?.length
70
- ? workspace.locales
71
- : [defaultLocaleOf(workspace.locales)],
72
67
  contentTypes: models.map((m) => m.type),
73
68
  contentModels: models.map((m) => ({
74
69
  type: m.type,
@@ -1,35 +1,23 @@
1
1
  // packages/studio/src/pages/preview-url.ts
2
- import {
3
- encodeDocLocale,
4
- PREVIEW_LOCALE_QUERY,
5
- revalidateUrl,
6
- routeFor,
7
- routeTemplate,
8
- type RouteModel,
9
- } from "@backmatter/core";
2
+ import { revalidateUrl, routeFor, type RouteModel } from "@backmatter/core";
10
3
 
11
4
  export function modelForType(models: readonly RouteModel[], type: string): RouteModel {
12
5
  return models.find((m) => m.type === type) ?? { type };
13
6
  }
14
7
 
15
- export function publicDocPath(model: RouteModel, locale: string, slug: string): string {
16
- return routeFor(model, locale, slug);
8
+ export function publicDocPath(model: RouteModel, slug: string): string {
9
+ return routeFor(model, slug);
17
10
  }
18
11
 
19
12
  /** Public path once checkout is known. Null holds the iframe off HEAD. */
20
13
  export function editorPreviewPath(
21
14
  model: RouteModel,
22
15
  slug: string,
23
- locale = "en",
24
16
  branch?: string | null,
25
17
  tree?: string | null,
26
18
  ): string | null {
27
19
  if (!branch?.trim() || !tree?.trim()) return null;
28
- const path = publicDocPath(model, locale, slug);
29
- if (!path || routeTemplate(model).includes("{locale}")) return path || null;
30
- const params = new URLSearchParams();
31
- params.set(PREVIEW_LOCALE_QUERY, encodeDocLocale(locale));
32
- return `${path}?${params.toString()}`;
20
+ return publicDocPath(model, slug) || null;
33
21
  }
34
22
 
35
23
  /**
@@ -80,12 +68,11 @@ export function primarySiteUrl(
80
68
  export function productionDocUrl(
81
69
  siteUrl: string | null | undefined,
82
70
  model: RouteModel,
83
- locale: string,
84
71
  slug: string,
85
72
  ): string | null {
86
73
  const origin = siteUrl?.trim().replace(/\/$/, "");
87
74
  if (!origin) return null;
88
- return `${origin}${publicDocPath(model, locale, slug)}`;
75
+ return `${origin}${publicDocPath(model, slug)}`;
89
76
  }
90
77
 
91
78
  export function revalidateWebhookUrl(siteUrl: string | null | undefined): string | null {
@@ -108,11 +95,10 @@ export function buildPreviewSiteUrl(
108
95
  base: string,
109
96
  model: RouteModel,
110
97
  slug: string,
111
- locale = "en",
112
98
  branch?: string | null,
113
99
  tree?: string | null,
114
100
  ticket?: string | null,
115
101
  ): string | null {
116
- if (!editorPreviewPath(model, slug, locale, branch, tree) || !ticket) return null;
102
+ if (!editorPreviewPath(model, slug, branch, tree) || !ticket) return null;
117
103
  return previewEnableUrl(base, ticket);
118
104
  }
@@ -1,9 +1,6 @@
1
1
  // packages/studio/src/pages/type-docs.ts
2
2
 
3
- /** Slug order. A missing locale is not a language — data models omit it. */
4
- export function compareTypeDocs(
5
- a: { slug: string; locale?: string },
6
- b: { slug: string; locale?: string },
7
- ) {
8
- return a.slug.localeCompare(b.slug) || (a.locale ?? "").localeCompare(b.locale ?? "");
3
+ /** Slug order for type index lists. */
4
+ export function compareTypeDocs(a: { slug: string }, b: { slug: string }) {
5
+ return a.slug.localeCompare(b.slug);
9
6
  }
@@ -38,10 +38,16 @@ export function guessPreviewFrameRadius(): number {
38
38
  export function resolvePreviewFrameRadius(override?: number): number {
39
39
  if (override !== undefined && Number.isFinite(override) && override >= 0) return override;
40
40
  if (typeof document !== "undefined") {
41
- const raw = getComputedStyle(document.documentElement).getPropertyValue("--bm-preview-frame-radius").trim();
42
- if (raw) {
43
- const parsed = Number.parseFloat(raw);
44
- if (Number.isFinite(parsed) && parsed >= 0) return parsed;
41
+ try {
42
+ const raw = getComputedStyle(document.documentElement)
43
+ .getPropertyValue("--bm-preview-frame-radius")
44
+ .trim();
45
+ if (raw) {
46
+ const parsed = Number.parseFloat(raw);
47
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
48
+ }
49
+ } catch {
50
+ // happy-dom and other minimal runtimes may not implement layout styles.
45
51
  }
46
52
  }
47
53
  return guessPreviewFrameRadius();
@@ -14,25 +14,19 @@ import type {
14
14
  } from "./types";
15
15
 
16
16
  function docKey(ref: DocRef) {
17
- return `${ref.locale}/${ref.type}/${ref.slug}`;
17
+ return `${ref.type}/${ref.slug}`;
18
18
  }
19
19
 
20
20
  const OVERLAY_STATUSES = new Set(["modified", "added", "conflict", "deleted"]);
21
21
 
22
22
  function validOverlay(row: ReplicaHit["overlay"][number]) {
23
- return Boolean(
24
- row.locale &&
25
- row.type &&
26
- row.slug &&
27
- OVERLAY_STATUSES.has(row.status),
28
- );
23
+ return Boolean(row.type && row.slug && OVERLAY_STATUSES.has(row.status));
29
24
  }
30
25
 
31
26
  function changesFromDocs(docs: DocListItem[]) {
32
27
  return docs
33
28
  .filter((doc): doc is DocListItem & { status: ChangeStatus } => doc.status !== "clean")
34
29
  .map((doc) => ({
35
- locale: doc.locale,
36
30
  type: doc.type,
37
31
  slug: doc.slug,
38
32
  status: doc.status,
@@ -308,7 +302,6 @@ export function createReplicaStore(
308
302
  pending.delete(docKey(doc));
309
303
  if (over.status === "deleted") continue;
310
304
  docs.push({
311
- locale: over.locale,
312
305
  type: over.type,
313
306
  slug: over.slug,
314
307
  status: over.status,
@@ -325,7 +318,6 @@ export function createReplicaStore(
325
318
  for (const over of pending.values()) {
326
319
  if (over.status === "deleted") continue;
327
320
  docs.push({
328
- locale: over.locale,
329
321
  type: over.type,
330
322
  slug: over.slug,
331
323
  status: over.status,
@@ -29,7 +29,6 @@ export type ReplicaSnapshot = {
29
29
  };
30
30
 
31
31
  export type ReplicaOverlayItem = {
32
- locale: string;
33
32
  type: string;
34
33
  slug: string;
35
34
  status: Exclude<DocStatus, "clean">;
@@ -77,7 +76,7 @@ export type ReplicaRecord = {
77
76
  checkouts: Record<string, ReplicaCheckout>;
78
77
  };
79
78
 
80
- export type DocRef = { locale: string; type: string; slug: string };
79
+ export type DocRef = { type: string; slug: string };
81
80
 
82
81
  export type ReplicaPersistence = {
83
82
  load(userId: string): Promise<ReplicaRecord | null>;
package/src/site-bar.tsx CHANGED
@@ -61,7 +61,6 @@ const DEFAULT_PREVIEW_ENTRY = "/api/backmatter/preview";
61
61
  export type { DevbarActivity, DevbarStep };
62
62
 
63
63
  export type PageMenuDoc = {
64
- locale: string;
65
64
  type: string;
66
65
  slug: string;
67
66
  };
@@ -184,7 +183,6 @@ export function SiteBar({
184
183
  const [settingsPane, setSettingsPane] = useState<"main" | "shortcuts">("main");
185
184
  const [hostName, setHostName] = useState("");
186
185
  const [workspaceSlug, setWorkspaceSlug] = useState<string | null>(null);
187
- const [workspaceDefaultLocale, setWorkspaceDefaultLocale] = useState("en");
188
186
 
189
187
  useEffect(() => {
190
188
  const id = doc?.workspaceId;
@@ -197,7 +195,6 @@ export function SiteBar({
197
195
  .then(({ workspace }) => {
198
196
  if (cancelled) return;
199
197
  setWorkspaceSlug(workspace.slug);
200
- setWorkspaceDefaultLocale(workspace.locales[0] ?? "en");
201
198
  })
202
199
  .catch(() => {
203
200
  if (!cancelled) setWorkspaceSlug(null);
@@ -208,8 +205,8 @@ export function SiteBar({
208
205
  }, [doc?.workspaceId]);
209
206
 
210
207
  const dashboardOpenHref = useMemo(
211
- () => resolveDashboardOpenHref(dashboardHref, doc, workspaceSlug, workspaceDefaultLocale),
212
- [dashboardHref, doc, workspaceSlug, workspaceDefaultLocale],
208
+ () => resolveDashboardOpenHref(dashboardHref, doc, workspaceSlug),
209
+ [dashboardHref, doc, workspaceSlug],
213
210
  );
214
211
 
215
212
  const setBar = useCallback((node: HTMLElement | null) => {
@@ -478,7 +475,7 @@ export function SiteBar({
478
475
  try {
479
476
  const { workspace } = await fetchWorkspace(doc.workspaceId);
480
477
  href =
481
- resolveDashboardOpenHref(dashboardHref, doc, workspace.slug, workspace.locales[0] ?? "en") ??
478
+ resolveDashboardOpenHref(dashboardHref, doc, workspace.slug) ??
482
479
  href;
483
480
  } catch {
484
481
  // Keep the dashboard origin.
@@ -2,7 +2,6 @@
2
2
  import { contentDocPath } from "./workspace-path";
3
3
 
4
4
  export type DashboardDocRef = {
5
- locale: string;
6
5
  type: string;
7
6
  slug: string;
8
7
  };
@@ -12,10 +11,9 @@ export function resolveDashboardOpenHref(
12
11
  dashboardOrigin: string | undefined,
13
12
  doc: DashboardDocRef | null,
14
13
  workspaceSlug: string | null,
15
- defaultLocale = "en",
16
14
  ): string | undefined {
17
15
  if (!dashboardOrigin?.trim()) return undefined;
18
16
  const base = dashboardOrigin.replace(/\/+$/, "");
19
17
  if (!doc || !workspaceSlug) return base;
20
- return `${base}${contentDocPath(workspaceSlug, doc.type, doc.slug, doc.locale, defaultLocale)}`;
18
+ return `${base}${contentDocPath(workspaceSlug, doc.type, doc.slug)}`;
21
19
  }