@backmatter/studio 0.0.12 → 0.0.14

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.
@@ -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
  >
@@ -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
  }
@@ -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();
@@ -21,7 +21,7 @@ const OVERLAY_STATUSES = new Set(["modified", "added", "conflict", "deleted"]);
21
21
 
22
22
  function validOverlay(row: ReplicaHit["overlay"][number]) {
23
23
  return Boolean(
24
- row.locale &&
24
+ typeof row.locale === "string" &&
25
25
  row.type &&
26
26
  row.slug &&
27
27
  OVERLAY_STATUSES.has(row.status),
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
  }
@@ -7,18 +7,8 @@ export function contentTypePath(slug: string, type: string) {
7
7
  return `/${slug}/content/${type}`;
8
8
  }
9
9
 
10
- export function contentDocPath(
11
- slug: string,
12
- type: string,
13
- docSlug: string,
14
- locale: string,
15
- defaultLocale: string,
16
- ) {
17
- const path = `/${slug}/content/${type}/${docSlug}`;
18
- if (locale && locale !== defaultLocale) {
19
- return `${path}?locale=${encodeURIComponent(locale)}`;
20
- }
21
- return path;
10
+ export function contentDocPath(slug: string, type: string, docSlug: string) {
11
+ return `/${slug}/content/${type}/${docSlug}`;
22
12
  }
23
13
 
24
14
  export function mediaPath(slug: string) {
@@ -50,11 +40,7 @@ export function cmsWorkspaceSlug(pathname: string): string | null {
50
40
  return first;
51
41
  }
52
42
 
53
- export function rewriteLegacyWorkspacePath(
54
- pathname: string,
55
- slug: string,
56
- defaultLocale = "en",
57
- ): string {
43
+ export function rewriteLegacyWorkspacePath(pathname: string, slug: string): string {
58
44
  const rest = pathname.match(/^\/workspaces\/[^/]+(?:\/(.*))?$/)?.[1] ?? "";
59
45
  if (!rest) return workspaceHomePath(slug);
60
46
  const types = rest.match(/^types\/([^/]+)\/?$/);
@@ -65,8 +51,6 @@ export function rewriteLegacyWorkspacePath(
65
51
  slug,
66
52
  decodeURIComponent(docs[2]!),
67
53
  decodeURIComponent(docs[3]!),
68
- decodeURIComponent(docs[1]!),
69
- defaultLocale,
70
54
  );
71
55
  }
72
56
  if (rest === "github" || rest.startsWith("settings/")) {
@@ -75,10 +59,6 @@ export function rewriteLegacyWorkspacePath(
75
59
  return workspaceHomePath(slug);
76
60
  }
77
61
 
78
- export function defaultLocaleOf(locales: string[] | null | undefined) {
79
- return locales?.[0] || "en";
80
- }
81
-
82
62
  export function isWorkspaceEditorPath(pathname: string) {
83
63
  return /\/content\/[^/]+\/[^/]+\/?$/.test(pathname);
84
64
  }