@backmatter/studio 0.0.9 → 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.
- package/dist/chunks/{UiKitApp-b8qFsmA-.js → UiKitApp-Cu0CS1NU.js} +195 -186
- package/dist/chunks/{WorkspaceRoutes-BKKEkAq7.js → WorkspaceRoutes-Dzpaj8ov.js} +15186 -15183
- package/dist/chunks/{dashboard-BNUcbYU0.js → dashboard-kWmpeuAP.js} +2 -2
- package/dist/chunks/{index-laWAz5_p.js → index-Bryt_VJQ.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +2 -2
- package/dist/site-widget.js +84 -75
- package/package.json +1 -1
- package/src/api.ts +1 -1
- package/src/chrome/WorkspaceStatusBar.tsx +7 -6
- package/src/chrome/branch-head.ts +14 -0
- package/src/pages/DocEditorPage.tsx +26 -22
- package/src/pages/preview-url.ts +15 -0
- package/src/preview-frame.tsx +19 -4
- package/src/site-bar.tsx +21 -2
|
@@ -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 (!
|
|
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,
|
|
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(
|
|
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
|
-
}, [
|
|
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
|
|
543
|
-
//
|
|
544
|
-
//
|
|
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)
|
package/src/pages/preview-url.ts
CHANGED
|
@@ -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.
|
package/src/preview-frame.tsx
CHANGED
|
@@ -12,6 +12,22 @@ import { PREVIEW_FRAME_RADIUS_DEFAULT, previewFrameRadiusFromUserAgent } from ".
|
|
|
12
12
|
|
|
13
13
|
export const PREVIEW_FRAME_CLASS = "backmatter-preview-frame";
|
|
14
14
|
|
|
15
|
+
type NavigatorWithUaData = Navigator & {
|
|
16
|
+
userAgentData?: {
|
|
17
|
+
getHighEntropyValues: (
|
|
18
|
+
hints: readonly string[],
|
|
19
|
+
) => Promise<{ platformVersion?: string }>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Chromium `platformVersion` when Client Hints are available (not in all TS `Navigator` libs). */
|
|
24
|
+
function readNavigatorPlatformVersion(): Promise<string | undefined> {
|
|
25
|
+
if (typeof navigator === "undefined") return Promise.resolve(undefined);
|
|
26
|
+
const uad = (navigator as NavigatorWithUaData).userAgentData;
|
|
27
|
+
if (!uad?.getHighEntropyValues) return Promise.resolve(undefined);
|
|
28
|
+
return uad.getHighEntropyValues(["platformVersion"]).then((hints) => hints.platformVersion);
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
/** Bottom window corner radius when the host does not set `devBar.previewFrameRadius`. */
|
|
16
32
|
export function guessPreviewFrameRadius(): number {
|
|
17
33
|
if (typeof navigator === "undefined") return PREVIEW_FRAME_RADIUS_DEFAULT;
|
|
@@ -54,10 +70,9 @@ export function PreviewFrame({
|
|
|
54
70
|
const ua = navigator.userAgent;
|
|
55
71
|
setCorner(previewFrameRadiusFromUserAgent(ua));
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
setCorner(previewFrameRadiusFromUserAgent(ua, hints.platformVersion));
|
|
73
|
+
void readNavigatorPlatformVersion().then((platformVersion) => {
|
|
74
|
+
if (platformVersion === undefined) return;
|
|
75
|
+
setCorner(previewFrameRadiusFromUserAgent(ua, platformVersion));
|
|
61
76
|
});
|
|
62
77
|
}, [radius]);
|
|
63
78
|
|
package/src/site-bar.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useMemo,
|
|
11
11
|
useRef,
|
|
12
12
|
useState,
|
|
13
|
+
type HTMLAttributes,
|
|
13
14
|
type MouseEvent,
|
|
14
15
|
type ReactNode,
|
|
15
16
|
type Ref,
|
|
@@ -74,6 +75,14 @@ function pageMenuCacheKey(workspaceId: string, query: string) {
|
|
|
74
75
|
|
|
75
76
|
type MenuState = "page" | "branch" | "settings" | null;
|
|
76
77
|
|
|
78
|
+
type DevbarHotkeyHandlers = {
|
|
79
|
+
toggleHidden: () => void;
|
|
80
|
+
onModeKey: (origin: HTMLElement) => void;
|
|
81
|
+
onEscape: () => void;
|
|
82
|
+
openDashboard: () => boolean;
|
|
83
|
+
openShortcuts: () => void;
|
|
84
|
+
};
|
|
85
|
+
|
|
77
86
|
export function SiteBar({
|
|
78
87
|
label,
|
|
79
88
|
branch,
|
|
@@ -161,7 +170,7 @@ export function SiteBar({
|
|
|
161
170
|
const widthRef = useRef(0);
|
|
162
171
|
const widthReady = useRef(false);
|
|
163
172
|
const rose = useRef(false);
|
|
164
|
-
const handlers = useRef({
|
|
173
|
+
const handlers = useRef<DevbarHotkeyHandlers>({
|
|
165
174
|
toggleHidden: () => {},
|
|
166
175
|
onModeKey: (_origin: HTMLElement) => {},
|
|
167
176
|
onEscape: () => {},
|
|
@@ -703,7 +712,17 @@ export function SiteBar({
|
|
|
703
712
|
return (
|
|
704
713
|
<div ref={attachHost} className={`${DEVBAR_CLASS}-host`} data-backmatter-devbar="">
|
|
705
714
|
<style>{LIGHT_CSS}</style>
|
|
706
|
-
{shadow
|
|
715
|
+
{shadow
|
|
716
|
+
? createPortal(body, shadow)
|
|
717
|
+
: (
|
|
718
|
+
<template
|
|
719
|
+
{...({ shadowrootmode: "open" } as HTMLAttributes<HTMLTemplateElement> & {
|
|
720
|
+
shadowrootmode?: "open" | "closed";
|
|
721
|
+
})}
|
|
722
|
+
>
|
|
723
|
+
{body}
|
|
724
|
+
</template>
|
|
725
|
+
)}
|
|
707
726
|
</div>
|
|
708
727
|
);
|
|
709
728
|
}
|