@backmatter/studio 0.0.10 → 0.0.11

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.
@@ -3,7 +3,7 @@ var Xe = (e, t, n) => t in e ? Ke(e, t, { enumerable: !0, configurable: !0, writ
3
3
  var D = (e, t, n) => Xe(e, typeof t != "symbol" ? t + "" : t, n);
4
4
  import { jsxs as k, jsx as o, Fragment as be } from "react/jsx-runtime";
5
5
  import { useRef as Ye, useCallback as Qe, useSyncExternalStore as Ze, useState as w, useEffect as V, lazy as et, Suspense as tt } from "react";
6
- import { O as nt, ag as ye, ah as rt, K as _, ai as st, p as it, aj as at, ak as Ne, al as Ce, am as Ie, an as ot, ao as lt, N as qe, ap as ct, aq as ie, aa as J, Q as ze, ar as ut, as as Fe, ab as dt, ad as re, a4 as Q, a5 as Z, P as ae, at as ft, M as z, au as ht, z as ve, av as ee, ac as pt, aw as gt, ax as mt, ay as xt, f as yt, az as vt, a0 as bt, aA as wt, aB as kt, q as St, B as Et, y as Rt, aC as _t, aD as Ot, aE as Tt, A as Pt, aF as Lt, R as At, F as jt, G as Ut, H as Nt, I as F, L as Ct, J as It } from "./WorkspaceRoutes-BKKEkAq7.js";
6
+ import { O as nt, ag as ye, ah as rt, K as _, ai as st, p as it, aj as at, ak as Ne, al as Ce, am as Ie, an as ot, ao as lt, N as qe, ap as ct, aq as ie, aa as J, Q as ze, ar as ut, as as Fe, ab as dt, ad as re, a4 as Q, a5 as Z, P as ae, at as ft, M as z, au as ht, z as ve, av as ee, ac as pt, aw as gt, ax as mt, ay as xt, f as yt, az as vt, a0 as bt, aA as wt, aB as kt, q as St, B as Et, y as Rt, aC as _t, aD as Ot, aE as Tt, A as Pt, aF as Lt, R as At, F as jt, G as Ut, H as Nt, I as F, L as Ct, J as It } from "./WorkspaceRoutes-Dzpaj8ov.js";
7
7
  const te = /* @__PURE__ */ Object.create(null), W = (e) => {
8
8
  var t, n;
9
9
  return ((t = globalThis.process) == null ? void 0 : t.env) || ((n = globalThis.Deno) == null ? void 0 : n.env.toObject()) || globalThis.__env__ || (e ? te : globalThis);
@@ -1701,7 +1701,7 @@ function kr(e) {
1701
1701
  const t = e.replace(/\/$/, "") || "/";
1702
1702
  return t === "/__ui" || /^\/__ui\/[^/]+$/.test(t);
1703
1703
  }
1704
- const Sr = et(() => import("./UiKitApp-BxvTwsko.js").then((e) => ({
1704
+ const Sr = et(() => import("./UiKitApp-Cu0CS1NU.js").then((e) => ({
1705
1705
  default: e.UiKitApp
1706
1706
  })));
1707
1707
  function Pr() {
@@ -1,7 +1,7 @@
1
1
  var Rs = Object.defineProperty;
2
2
  var Es = (r, e, t) => e in r ? Rs(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
3
3
  var M = (r, e, t) => Es(r, typeof e != "symbol" ? e + "" : e, t);
4
- import { aG as Bt, aH as Bs, aI as a0, aJ as ct, aK as xt, aL as ea, aM as Gn, aN as Is, aO as Ls, aP as Ds, aQ as La, aR as Ps, aS as Os, aT as Le, aU as qs } from "./WorkspaceRoutes-BKKEkAq7.js";
4
+ import { aG as Bt, aH as Bs, aI as a0, aJ as ct, aK as xt, aL as ea, aM as Gn, aN as Is, aO as Ls, aP as Ds, aQ as La, aR as Ps, aS as Os, aT as Le, aU as qs } from "./WorkspaceRoutes-Dzpaj8ov.js";
5
5
  function Fs() {
6
6
  return {
7
7
  enter: {
package/dist/dashboard.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as a, i as s } from "./chunks/dashboard-8rjkfEXl.js";
2
- import { w as t, x as f } from "./chunks/WorkspaceRoutes-BKKEkAq7.js";
1
+ import { A as a, i as s } from "./chunks/dashboard-kWmpeuAP.js";
2
+ import { w as t, x as f } from "./chunks/WorkspaceRoutes-Dzpaj8ov.js";
3
3
  export {
4
4
  a as DashboardApp,
5
5
  t as configureStudio,
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as t, jsxs as d } from "react/jsx-runtime";
2
2
  import { useLayoutEffect as re, useEffect as f, useRef as W, useState as g, useMemo as oe } from "react";
3
- import { i as J, T as ae, E as se, C as ce, a as le, b as de, u as ue, D as A, c as X, d as B, e as xe, f as Y, g as G, h as me, p as z, j as C, k as N, r as fe, n as he, s as pe, l as ge, m as we, o as be, q as ye, B as ve, t as ke, S as qe, v as Ee, w as Q, x as P, y as ze, z as Ce, A as Ne, R as Se, F as Re, G as Le, H as Pe, I as S, N as I, L as Ue, J as je, K as F, M as Te } from "./chunks/WorkspaceRoutes-BKKEkAq7.js";
4
- import { O as ot } from "./chunks/WorkspaceRoutes-BKKEkAq7.js";
3
+ import { i as J, T as ae, E as se, C as ce, a as le, b as de, u as ue, D as A, c as X, d as B, e as xe, f as Y, g as G, h as me, p as z, j as C, k as N, r as fe, n as he, s as pe, l as ge, m as we, o as be, q as ye, B as ve, t as ke, S as qe, v as Ee, w as Q, x as P, y as ze, z as Ce, A as Ne, R as Se, F as Re, G as Le, H as Pe, I as S, N as I, L as Ue, J as je, K as F, M as Te } from "./chunks/WorkspaceRoutes-Dzpaj8ov.js";
4
+ import { O as ot } from "./chunks/WorkspaceRoutes-Dzpaj8ov.js";
5
5
  import { u as Me, S as _e } from "./chunks/define-studio-BYtYDGLX.js";
6
6
  import { d as st } from "./chunks/define-studio-BYtYDGLX.js";
7
7
  const R = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/studio",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
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": {
package/src/api.ts CHANGED
@@ -501,7 +501,7 @@ export function notifyTreeChanged(detail?: TreeChangeDetail) {
501
501
  );
502
502
  }
503
503
 
504
- /** Branch HEAD moved (teammate push, pull, commit); clean docs should reload. */
504
+ /** Branch HEAD moved (teammate push, pull, commit). Clean docs reload their fields. The preview frame stays up. */
505
505
  export function notifyHeadChanged() {
506
506
  window.dispatchEvent(new Event("backmatter:head-changed"));
507
507
  }
@@ -35,9 +35,11 @@ import { replicaStore } from "../replica/instance";
35
35
  import { useReplicaSession } from "../replica/ReplicaProvider";
36
36
  import { useReplicaState } from "../replica/useReplica";
37
37
  import { useWorkspaceNav } from "./WorkspaceNavContext";
38
+ import { branchHeadMoved } from "./branch-head";
38
39
  import { commitTriggerDisabled, statusBarAction } from "./status-bar-action";
39
40
 
40
41
  const hover = "@media (hover: hover)";
42
+ /** Status bar only. The preview frame follows HEAD on its SSE stream, not on this poll. */
41
43
  const POLL_MS = 8000;
42
44
 
43
45
  export const STATUS_LABEL: Record<ChangeStatus, string> = {
@@ -181,7 +183,8 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
181
183
  const [resolvingKey, setResolvingKey] = useState<string | null>(null);
182
184
  const [message, setMessage] = useState("");
183
185
  const [error, setError] = useState<string | null>(null);
184
- // HEAD identity from the last poll so we can announce "HEAD moved".
186
+ // Commit sha from the last poll. A new sha means HEAD moved. The preview
187
+ // iframe does not reload for that; its page follows the SSE head event.
185
188
  const lastHead = useRef<string | null | undefined>(undefined);
186
189
 
187
190
  async function reloadGit() {
@@ -195,11 +198,9 @@ export function WorkspaceStatusBar({ workspaceId }: { workspaceId: string }) {
195
198
  } else {
196
199
  setError(null);
197
200
  }
198
- const headKey = next.head ? `${next.head.sha}:${next.head.updatedAt}` : null;
199
- if (lastHead.current !== undefined && lastHead.current !== headKey) {
200
- notifyHeadChanged();
201
- }
202
- lastHead.current = headKey;
201
+ const nextSha = next.head?.sha ?? null;
202
+ if (branchHeadMoved(lastHead.current, nextSha)) notifyHeadChanged();
203
+ lastHead.current = nextSha;
203
204
  } catch (err) {
204
205
  setError(err instanceof Error ? err.message : "Git status failed");
205
206
  }
@@ -0,0 +1,14 @@
1
+ // packages/studio/src/chrome/branch-head.ts
2
+
3
+ /**
4
+ * Whether a later git poll is a new commit.
5
+ * The first sample only records the sha. `updatedAt` is not part of the
6
+ * identity: rewriting the head row must not look like HEAD moved.
7
+ */
8
+ export function branchHeadMoved(
9
+ previousSha: string | null | undefined,
10
+ nextSha: string | null,
11
+ ): boolean {
12
+ if (previousSha === undefined) return false;
13
+ return previousSha !== nextSha;
14
+ }
@@ -49,9 +49,10 @@ import {
49
49
  import {
50
50
  editorPreviewPath,
51
51
  modelForType,
52
- previewSiteUrl,
53
- primarySiteUrl,
54
52
  onSitePreviewUrl,
53
+ previewEnableUrl,
54
+ previewFrameIdentity,
55
+ primarySiteUrl,
55
56
  studioIsOnSite,
56
57
  productionDocUrl,
57
58
  } from "./preview-url";
@@ -255,31 +256,35 @@ export function DocEditorPage() {
255
256
  fieldNames.find((name) => fieldKinds[name] === "blocks") ?? "blocks";
256
257
  const siteUrl = primarySiteUrl(workspace.siteUrls ?? []);
257
258
  const liveUrl = productionDocUrl(siteUrl, modelForType(models, type), locale, slug);
259
+ const previewPath = editorPreviewPath(
260
+ modelForType(models, type),
261
+ slug,
262
+ locale,
263
+ branch,
264
+ tree,
265
+ );
266
+ // Strings only. A replica refresh builds new models and siteUrls arrays
267
+ // with the same contents; that must not mint a ticket and reload the frame.
268
+ const frameIdentity = previewFrameIdentity({
269
+ siteUrl,
270
+ path: showPreview ? previewPath : null,
271
+ branch,
272
+ tree,
273
+ });
258
274
 
259
275
  useEffect(() => {
260
- if (!showPreview) {
261
- setPreviewUrl(null);
262
- return;
263
- }
264
- const redirect = editorPreviewPath(
265
- modelForType(models, type),
266
- slug,
267
- locale,
268
- branch,
269
- tree,
270
- );
271
- if (!redirect || !workspaceId || !siteUrl) {
276
+ if (!frameIdentity || !previewPath || !workspaceId || !siteUrl) {
272
277
  setPreviewUrl(null);
273
278
  return;
274
279
  }
275
280
  if (studioIsOnSite(siteUrl, window.location.origin)) {
276
- setPreviewUrl(onSitePreviewUrl(siteUrl, redirect));
281
+ setPreviewUrl(onSitePreviewUrl(siteUrl, previewPath));
277
282
  return;
278
283
  }
279
284
  let cancelled = false;
280
- void createPreviewTicket(workspaceId, { redirect, branch, tree })
285
+ void createPreviewTicket(workspaceId, { redirect: previewPath, branch, tree })
281
286
  .then((res) => {
282
- if (!cancelled) setPreviewUrl(previewSiteUrl(workspace.siteUrls ?? [], res.ticket));
287
+ if (!cancelled) setPreviewUrl(previewEnableUrl(siteUrl, res.ticket));
283
288
  })
284
289
  .catch(() => {
285
290
  if (!cancelled) setPreviewUrl(null);
@@ -287,7 +292,7 @@ export function DocEditorPage() {
287
292
  return () => {
288
293
  cancelled = true;
289
294
  };
290
- }, [showPreview, workspaceId, workspace.siteUrls, siteUrl, models, type, slug, locale, branch, tree]);
295
+ }, [frameIdentity, previewPath, workspaceId, siteUrl, branch, tree]);
291
296
 
292
297
  useEffect(() => {
293
298
  setPreviewLoading(true);
@@ -539,13 +544,12 @@ export function DocEditorPage() {
539
544
  setDocStatus(detail.status);
540
545
  setSource(detail.status === "clean" ? "head" : "tree");
541
546
  }
542
- // Branch HEAD moved (teammate push / pull / commit). A clean doc reloads to
543
- // show the new HEAD; a doc I am editing keeps my text and just refreshes its
544
- // status, which the server reports as `conflict` when HEAD touched this file.
547
+ // Branch HEAD moved (teammate push / pull / commit). A clean doc reloads its
548
+ // fields. The preview frame stays mounted: the page follows the same move
549
+ // on its SSE stream and refreshes in place.
545
550
  function onHeadChanged() {
546
551
  if (docStatusRef.current === "clean") {
547
552
  onChanged();
548
- remountPreview();
549
553
  return;
550
554
  }
551
555
  void fetchDoc(workspaceId, locale, type, slug)
@@ -32,6 +32,21 @@ export function editorPreviewPath(
32
32
  return `${path}?${params.toString()}`;
33
33
  }
34
34
 
35
+ /**
36
+ * Identity of the preview iframe. Equal results must keep the frame mounted.
37
+ * A replacement models array or siteUrls array is not a new target; the page
38
+ * inside the frame follows saves and HEAD on its SSE stream.
39
+ */
40
+ export function previewFrameIdentity(input: {
41
+ siteUrl: string | null;
42
+ path: string | null;
43
+ branch: string | null;
44
+ tree: string | null;
45
+ }): string | null {
46
+ if (!input.siteUrl || !input.path || !input.branch?.trim() || !input.tree?.trim()) return null;
47
+ return `${input.siteUrl}\0${input.branch}\0${input.tree}\0${input.path}`;
48
+ }
49
+
35
50
  /**
36
51
  * A Studio mounted on the site it previews already carries the site session
37
52
  * cookie, so the preview frame loads the page directly — no ticket.