@backmatter/studio 0.0.15 → 0.0.17
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-BDsrXN12.js → UiKitApp-D7vllRQE.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-DH1LD-2Q.js} +30013 -27965
- package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-Dsaeud2x.js} +55 -54
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-DHJvh5Uy.js → index-CGMqw2NH.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +49 -1
- package/dist/site-widget.js +8341 -8058
- package/dist/studio.css +279 -13
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +82 -7
- package/src/chat/ChangeCard.tsx +8 -2
- package/src/chat/ChatMessage.tsx +171 -102
- package/src/chat/ChatPanel.tsx +54 -34
- package/src/chat/ChatProvider.tsx +122 -20
- package/src/chat/Composer.tsx +51 -8
- package/src/chat/PointerChip.tsx +9 -5
- package/src/chat/ThreadHistory.tsx +3 -3
- package/src/chat/chat-context.ts +67 -3
- package/src/chat/chat-helpers.ts +12 -0
- package/src/chat/chat-state.ts +11 -0
- package/src/chrome/AppShell.tsx +16 -5
- package/src/chrome/CommandPalette.tsx +12 -0
- package/src/chrome/ContentExplorerSidebar.tsx +23 -3
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +221 -30
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/content-api-url.ts +0 -78
- package/src/chrome/content-tree-visibility.ts +13 -3
- package/src/chrome/explorer-icons.tsx +5 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -0
- package/src/fields/FieldControl.tsx +748 -0
- package/src/fields/choose-field-input.ts +64 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/host-pointer-bootstrap.ts +11 -0
- package/src/host-pointer.ts +147 -0
- package/src/install-preview-pointer.ts +19 -3
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1361 -0
- package/src/pages/DocEditorPage.tsx +120 -107
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +53 -0
- package/src/pages/code-session-files.ts +42 -0
- package/src/pages/code-session-frame.ts +52 -0
- package/src/pages/code-session-routes.ts +42 -0
- package/src/pages/home-rows.ts +140 -0
- package/src/pages/preview-size.ts +33 -0
- package/src/pages/session-preview.ts +109 -0
- package/src/pages/use-session-preview.ts +70 -0
- package/src/replica/store.ts +11 -0
- package/src/replica/types.ts +6 -0
- package/src/shell-resets.css +15 -0
- package/src/site-pointer.ts +24 -5
- package/src/studio-config.ts +1 -1
- package/src/ui/Input.tsx +178 -20
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/ui/popup.ts +11 -2
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// packages/studio/src/pages/home-rows.ts
|
|
2
|
+
import type { ChangeStatus, RecentCommit } from "../api";
|
|
3
|
+
import { routeFor } from "@backmatter/core";
|
|
4
|
+
import { docTreeLabel } from "../chrome/content-repo-path";
|
|
5
|
+
import type { ContentTypeNav } from "../chrome/content-type-groups";
|
|
6
|
+
import { contentDocPath, settingsPath } from "../workspace-path";
|
|
7
|
+
|
|
8
|
+
const STATUS_WORD: Record<ChangeStatus, string> = {
|
|
9
|
+
modified: "modified",
|
|
10
|
+
added: "new",
|
|
11
|
+
conflict: "conflict",
|
|
12
|
+
deleted: "deleted",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type HomeChange = {
|
|
16
|
+
type: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
status: ChangeStatus;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type HomeModel = Pick<ContentTypeNav, "type" | "path"> & {
|
|
23
|
+
route?: string | false;
|
|
24
|
+
indexSlug?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type HomeRow = {
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
/** Public path when the model is routable. Null for data with no route. */
|
|
31
|
+
route: string | null;
|
|
32
|
+
status: string;
|
|
33
|
+
href: string | null;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Public path for a changed document. Data models with `route: false` have none. */
|
|
38
|
+
export function changedRoute(
|
|
39
|
+
change: Pick<HomeChange, "type" | "slug">,
|
|
40
|
+
model: Pick<HomeModel, "type" | "route" | "indexSlug"> | undefined,
|
|
41
|
+
): string | null {
|
|
42
|
+
if (model?.route === false) return null;
|
|
43
|
+
const path = routeFor(
|
|
44
|
+
{ type: change.type, route: model?.route, indexSlug: model?.indexSlug },
|
|
45
|
+
change.slug,
|
|
46
|
+
);
|
|
47
|
+
return path || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Working-tree rows for the workspace home, newest edit first. */
|
|
51
|
+
export function homeRows(
|
|
52
|
+
changes: HomeChange[],
|
|
53
|
+
models: HomeModel[],
|
|
54
|
+
workspaceSlug: string,
|
|
55
|
+
): HomeRow[] {
|
|
56
|
+
const rows = changes.map((change) => {
|
|
57
|
+
const model = models.find((item) => item.type === change.type);
|
|
58
|
+
const label = model ? docTreeLabel(model, change) : change.slug;
|
|
59
|
+
return {
|
|
60
|
+
key: `${change.type}/${change.slug}`,
|
|
61
|
+
label,
|
|
62
|
+
route: changedRoute(change, model),
|
|
63
|
+
status: STATUS_WORD[change.status],
|
|
64
|
+
href:
|
|
65
|
+
change.status === "deleted"
|
|
66
|
+
? null
|
|
67
|
+
: contentDocPath(workspaceSlug, change.type, change.slug),
|
|
68
|
+
updatedAt: change.updatedAt ?? "",
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
rows.sort((a, b) => {
|
|
72
|
+
if (a.updatedAt !== b.updatedAt) return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
73
|
+
return a.label.localeCompare(b.label);
|
|
74
|
+
});
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const RECENT_LIMIT = 4;
|
|
79
|
+
|
|
80
|
+
export type HomeSummary = {
|
|
81
|
+
name: string;
|
|
82
|
+
branch: string;
|
|
83
|
+
defaultBranch: string;
|
|
84
|
+
uncommitted: number;
|
|
85
|
+
conflicts: number;
|
|
86
|
+
recent: HomeRow[];
|
|
87
|
+
modelCount: number;
|
|
88
|
+
repository: string | null;
|
|
89
|
+
siteUrl: string | null;
|
|
90
|
+
schemaHref: string;
|
|
91
|
+
githubHref: string;
|
|
92
|
+
generalHref: string;
|
|
93
|
+
/** Null until git status has loaded. Empty means the branch has no commits. */
|
|
94
|
+
commits: RecentCommit[] | null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** How long ago a commit landed, stable enough to test. */
|
|
98
|
+
export function commitAge(iso: string, now: number): string {
|
|
99
|
+
const then = Date.parse(iso);
|
|
100
|
+
if (!Number.isFinite(then)) return "";
|
|
101
|
+
const minutes = Math.max(0, Math.round((now - then) / 60_000));
|
|
102
|
+
if (minutes < 1) return "just now";
|
|
103
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
104
|
+
const hours = Math.round(minutes / 60);
|
|
105
|
+
if (hours < 24) return `${hours}h ago`;
|
|
106
|
+
const days = Math.round(hours / 24);
|
|
107
|
+
if (days < 14) return `${days}d ago`;
|
|
108
|
+
return iso.slice(0, 10);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The few facts the workspace home can show without inventing activity. */
|
|
112
|
+
export function homeSummary(input: {
|
|
113
|
+
name: string;
|
|
114
|
+
slug: string;
|
|
115
|
+
branch: string;
|
|
116
|
+
defaultBranch: string;
|
|
117
|
+
repository: string | null;
|
|
118
|
+
siteUrl: string | null;
|
|
119
|
+
modelCount: number;
|
|
120
|
+
changes: HomeChange[];
|
|
121
|
+
models: HomeModel[];
|
|
122
|
+
commits?: RecentCommit[] | null;
|
|
123
|
+
}): HomeSummary {
|
|
124
|
+
const rows = homeRows(input.changes, input.models, input.slug);
|
|
125
|
+
return {
|
|
126
|
+
name: input.name,
|
|
127
|
+
branch: input.branch,
|
|
128
|
+
defaultBranch: input.defaultBranch,
|
|
129
|
+
uncommitted: rows.length,
|
|
130
|
+
conflicts: input.changes.filter((change) => change.status === "conflict").length,
|
|
131
|
+
recent: rows.slice(0, RECENT_LIMIT),
|
|
132
|
+
modelCount: input.modelCount,
|
|
133
|
+
repository: input.repository,
|
|
134
|
+
siteUrl: input.siteUrl,
|
|
135
|
+
schemaHref: settingsPath(input.slug, "schema"),
|
|
136
|
+
githubHref: settingsPath(input.slug, "github"),
|
|
137
|
+
generalHref: settingsPath(input.slug, "general"),
|
|
138
|
+
commits: input.commits ?? null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// packages/studio/src/pages/preview-size.ts
|
|
2
|
+
export type PreviewSize = "desktop" | "tablet" | "phone";
|
|
3
|
+
|
|
4
|
+
export const PREVIEW_SIZE_KEY = "backmatter-preview-size";
|
|
5
|
+
|
|
6
|
+
export type PreviewSizeStore = {
|
|
7
|
+
getItem(key: string): string | null;
|
|
8
|
+
setItem(key: string, value: string): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function previewFrameWidth(size: PreviewSize): "390px" | "768px" | null {
|
|
12
|
+
if (size === "phone") return "390px";
|
|
13
|
+
if (size === "tablet") return "768px";
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readPreviewSize(store?: PreviewSizeStore): PreviewSize {
|
|
18
|
+
try {
|
|
19
|
+
const raw = (store ?? localStorage).getItem(PREVIEW_SIZE_KEY);
|
|
20
|
+
if (raw === "desktop" || raw === "tablet" || raw === "phone") return raw;
|
|
21
|
+
} catch {
|
|
22
|
+
// Private mode, or no browser storage.
|
|
23
|
+
}
|
|
24
|
+
return "desktop";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function persistPreviewSize(size: PreviewSize, store?: PreviewSizeStore) {
|
|
28
|
+
try {
|
|
29
|
+
(store ?? localStorage).setItem(PREVIEW_SIZE_KEY, size);
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore quota and private mode.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// packages/studio/src/pages/session-preview.ts
|
|
2
|
+
// Studio side of pointing at the running host. Same preview channel as the
|
|
3
|
+
// document editor. The session never pushes a document into the page.
|
|
4
|
+
import { matchRoute, type RouteModel } from "@backmatter/core";
|
|
5
|
+
import {
|
|
6
|
+
createPreviewEditor,
|
|
7
|
+
type PreviewChannelRef,
|
|
8
|
+
type PreviewEditorSession,
|
|
9
|
+
type PreviewTransport,
|
|
10
|
+
} from "@backmatter/core/preview";
|
|
11
|
+
import type { ChatPointer } from "../chat/chat-context";
|
|
12
|
+
|
|
13
|
+
/** The document a hello or grab refers to. A route ref is resolved against the models. */
|
|
14
|
+
export function sessionDocument(
|
|
15
|
+
ref: PreviewChannelRef,
|
|
16
|
+
models: readonly RouteModel[],
|
|
17
|
+
): { type: string; slug: string } | null {
|
|
18
|
+
if (ref.type === "route") {
|
|
19
|
+
const path = ref.slug.split(/[?#]/, 1)[0] || "/";
|
|
20
|
+
const hit = matchRoute(models, path);
|
|
21
|
+
return hit ? { type: hit.type, slug: hit.slug } : null;
|
|
22
|
+
}
|
|
23
|
+
if (!ref.type || !ref.slug) return null;
|
|
24
|
+
return { type: ref.type, slug: ref.slug };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SessionPreviewBinding = {
|
|
28
|
+
arm(on: boolean): void;
|
|
29
|
+
highlight(blockId: string | null): void;
|
|
30
|
+
/** Ask a page that already loaded to send hello again. */
|
|
31
|
+
nudge(): void;
|
|
32
|
+
close(): void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function bindSessionPreview(options: {
|
|
36
|
+
transport: PreviewTransport;
|
|
37
|
+
onCanPoint(on: boolean): void;
|
|
38
|
+
onPointer(pointer: ChatPointer): void;
|
|
39
|
+
onDisarm(): void;
|
|
40
|
+
/** The document a hello or grab refers to. A route ref is resolved by the caller. */
|
|
41
|
+
documentAt(ref: PreviewChannelRef): { type: string; slug: string } | null;
|
|
42
|
+
}): SessionPreviewBinding {
|
|
43
|
+
let ref: PreviewChannelRef = { type: "_", slug: "_" };
|
|
44
|
+
let known = false;
|
|
45
|
+
let wantPoint = false;
|
|
46
|
+
let editor: PreviewEditorSession | null = null;
|
|
47
|
+
|
|
48
|
+
function remember(next: PreviewChannelRef) {
|
|
49
|
+
ref = {
|
|
50
|
+
type: next.type,
|
|
51
|
+
slug: next.slug,
|
|
52
|
+
...(next.branch ? { branch: next.branch } : {}),
|
|
53
|
+
...(next.tree ? { tree: next.tree } : {}),
|
|
54
|
+
};
|
|
55
|
+
known = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
editor = createPreviewEditor({
|
|
59
|
+
transport: options.transport,
|
|
60
|
+
ref: () => ref,
|
|
61
|
+
onHello(next, capabilities) {
|
|
62
|
+
remember(next);
|
|
63
|
+
options.onCanPoint(capabilities?.includes("point") ?? false);
|
|
64
|
+
editor?.point(wantPoint);
|
|
65
|
+
},
|
|
66
|
+
onGrab(grab) {
|
|
67
|
+
const doc = options.documentAt(grab.ref);
|
|
68
|
+
if (!doc) return;
|
|
69
|
+
options.onPointer({
|
|
70
|
+
type: doc.type,
|
|
71
|
+
slug: doc.slug,
|
|
72
|
+
blockId: grab.blockId,
|
|
73
|
+
blockType: grab.blockType,
|
|
74
|
+
blockIndex: grab.blockIndex,
|
|
75
|
+
quote: grab.quote,
|
|
76
|
+
...(grab.component ? { component: grab.component } : {}),
|
|
77
|
+
...(grab.file ? { file: grab.file } : {}),
|
|
78
|
+
...(grab.line ? { line: grab.line } : {}),
|
|
79
|
+
...(grab.column ? { column: grab.column } : {}),
|
|
80
|
+
...(grab.stack ? { stack: grab.stack } : {}),
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
onPoint(on) {
|
|
84
|
+
if (!on) {
|
|
85
|
+
wantPoint = false;
|
|
86
|
+
options.onDisarm();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
options.onCanPoint(false);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
arm(on) {
|
|
95
|
+
wantPoint = on;
|
|
96
|
+
if (known) editor?.point(on);
|
|
97
|
+
},
|
|
98
|
+
highlight(blockId) {
|
|
99
|
+
if (known) editor?.highlight(blockId);
|
|
100
|
+
},
|
|
101
|
+
nudge() {
|
|
102
|
+
if (!known) editor?.point(false);
|
|
103
|
+
},
|
|
104
|
+
close() {
|
|
105
|
+
editor?.close();
|
|
106
|
+
editor = null;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// packages/studio/src/pages/use-session-preview.ts
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import type { RouteModel } from "@backmatter/core";
|
|
4
|
+
import { POINTER_ARM_EVENT } from "../chat/pointer-arm";
|
|
5
|
+
import { useChatUIOptional } from "../chat/ChatProvider";
|
|
6
|
+
import { registerPreviewHighlight } from "../preview-highlight";
|
|
7
|
+
import { previewFrameTransport } from "./preview-bridge";
|
|
8
|
+
import { bindSessionPreview, sessionDocument } from "./session-preview";
|
|
9
|
+
|
|
10
|
+
/** Point at a block in the code session's host, the same way the document editor does. */
|
|
11
|
+
export function useSessionPreview(input: {
|
|
12
|
+
frame: HTMLIFrameElement | null;
|
|
13
|
+
origin: string | null;
|
|
14
|
+
models: readonly RouteModel[];
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}): void {
|
|
17
|
+
const ui = useChatUIOptional();
|
|
18
|
+
const uiRef = useRef(ui);
|
|
19
|
+
uiRef.current = ui;
|
|
20
|
+
const modelsRef = useRef(input.models);
|
|
21
|
+
modelsRef.current = input.models;
|
|
22
|
+
const armRef = useRef<(on: boolean) => void>(() => {});
|
|
23
|
+
const highlightRef = useRef<(blockId: string | null) => void>(() => {});
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!input.enabled) return;
|
|
27
|
+
const stop = registerPreviewHighlight((blockId) => highlightRef.current(blockId));
|
|
28
|
+
function onArm(event: Event) {
|
|
29
|
+
const on = (event as CustomEvent<{ on?: boolean }>).detail?.on === true;
|
|
30
|
+
armRef.current(on);
|
|
31
|
+
}
|
|
32
|
+
window.addEventListener(POINTER_ARM_EVENT, onArm);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener(POINTER_ARM_EVENT, onArm);
|
|
35
|
+
stop();
|
|
36
|
+
uiRef.current?.setPointerHighlight("local");
|
|
37
|
+
uiRef.current?.setPreviewPointer(false);
|
|
38
|
+
};
|
|
39
|
+
}, [input.enabled]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const chat = uiRef.current;
|
|
43
|
+
const frame = input.frame;
|
|
44
|
+
const origin = input.origin;
|
|
45
|
+
if (!input.enabled || !chat?.codeSessionId || !frame || !origin) return;
|
|
46
|
+
const transport = previewFrameTransport({ frame, siteOrigin: origin });
|
|
47
|
+
if (!transport) return;
|
|
48
|
+
chat.setPointerHighlight("channel");
|
|
49
|
+
const binding = bindSessionPreview({
|
|
50
|
+
transport,
|
|
51
|
+
onCanPoint: (on) => uiRef.current?.setPreviewPointer(on),
|
|
52
|
+
onPointer: (pointer) => uiRef.current?.addPointer(pointer),
|
|
53
|
+
onDisarm: () => {
|
|
54
|
+
window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on: false } }));
|
|
55
|
+
},
|
|
56
|
+
documentAt: (ref) => sessionDocument(ref, modelsRef.current),
|
|
57
|
+
});
|
|
58
|
+
armRef.current = (on) => binding.arm(on);
|
|
59
|
+
highlightRef.current = (blockId) => binding.highlight(blockId);
|
|
60
|
+
binding.nudge();
|
|
61
|
+
return () => {
|
|
62
|
+
armRef.current = () => {};
|
|
63
|
+
highlightRef.current = () => {};
|
|
64
|
+
binding.close();
|
|
65
|
+
transport.close();
|
|
66
|
+
window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on: false } }));
|
|
67
|
+
uiRef.current?.setPreviewPointer(false);
|
|
68
|
+
};
|
|
69
|
+
}, [input.enabled, input.frame, input.origin]);
|
|
70
|
+
}
|
package/src/replica/store.ts
CHANGED
|
@@ -165,6 +165,8 @@ export function createReplicaStore(
|
|
|
165
165
|
cached.head ??
|
|
166
166
|
(slot?.headSha ? { sha: slot.headSha, updatedAt: "" } : null),
|
|
167
167
|
changes: changesFromDocs(slot?.docs ?? []),
|
|
168
|
+
fetchedChanges: entry.git?.fetched,
|
|
169
|
+
recentCommits: entry.git?.recentCommits,
|
|
168
170
|
};
|
|
169
171
|
},
|
|
170
172
|
};
|
|
@@ -241,6 +243,7 @@ export function createReplicaStore(
|
|
|
241
243
|
const existing = checkouts[snapshot.workspace.id];
|
|
242
244
|
const defaultBranch =
|
|
243
245
|
snapshot.workspace.githubDefaultBranch ?? snapshot.checkout.branch;
|
|
246
|
+
const sameBranch = existing?.currentBranch === snapshot.checkout.branch;
|
|
244
247
|
checkouts = {
|
|
245
248
|
...checkouts,
|
|
246
249
|
[snapshot.workspace.id]: {
|
|
@@ -257,6 +260,12 @@ export function createReplicaStore(
|
|
|
257
260
|
: "",
|
|
258
261
|
}
|
|
259
262
|
: null,
|
|
263
|
+
...(sameBranch && existing?.git?.fetched
|
|
264
|
+
? { fetched: existing.git.fetched }
|
|
265
|
+
: {}),
|
|
266
|
+
...(sameBranch && existing?.git?.recentCommits
|
|
267
|
+
? { recentCommits: existing.git.recentCommits }
|
|
268
|
+
: {}),
|
|
260
269
|
},
|
|
261
270
|
branches: {
|
|
262
271
|
...(existing?.branches ?? {}),
|
|
@@ -400,6 +409,8 @@ export function createReplicaStore(
|
|
|
400
409
|
defaultBranch: git.defaultBranch,
|
|
401
410
|
branches: git.branches,
|
|
402
411
|
head: git.head,
|
|
412
|
+
fetched: git.changes,
|
|
413
|
+
recentCommits: git.recentCommits,
|
|
403
414
|
},
|
|
404
415
|
},
|
|
405
416
|
};
|
package/src/replica/types.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type {
|
|
3
3
|
DocListItem,
|
|
4
4
|
DocStatus,
|
|
5
|
+
GitChange,
|
|
5
6
|
GitState,
|
|
7
|
+
RecentCommit,
|
|
6
8
|
SchemaPayload,
|
|
7
9
|
WorkspaceDetail,
|
|
8
10
|
WorkspaceSummary,
|
|
@@ -61,6 +63,10 @@ export type ReplicaGitCache = {
|
|
|
61
63
|
defaultBranch: string;
|
|
62
64
|
branches: Array<{ name: string }>;
|
|
63
65
|
head: { sha: string; updatedAt: string } | null;
|
|
66
|
+
/** Git status changes for the current branch, once fetched. */
|
|
67
|
+
fetched?: GitChange[];
|
|
68
|
+
/** Latest commits on this branch, once git status has been fetched. */
|
|
69
|
+
recentCommits?: RecentCommit[];
|
|
64
70
|
};
|
|
65
71
|
|
|
66
72
|
export type ReplicaCheckout = {
|
package/src/shell-resets.css
CHANGED
|
@@ -150,10 +150,20 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
150
150
|
outline-color: #fafafa;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
[data-no-ring]:focus,
|
|
153
154
|
[data-no-ring]:focus-visible {
|
|
154
155
|
outline: none;
|
|
156
|
+
outline-offset: 0;
|
|
157
|
+
box-shadow: none;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
:is(
|
|
161
|
+
.backmatter-studio,
|
|
162
|
+
[data-backmatter-panel],
|
|
163
|
+
[data-base-ui-portal],
|
|
164
|
+
[data-selection-bar]
|
|
165
|
+
)
|
|
166
|
+
[data-no-ring]:focus,
|
|
157
167
|
:is(
|
|
158
168
|
.backmatter-studio,
|
|
159
169
|
[data-backmatter-panel],
|
|
@@ -161,8 +171,13 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
161
171
|
[data-selection-bar]
|
|
162
172
|
)
|
|
163
173
|
[data-no-ring]:focus-visible,
|
|
174
|
+
html[data-theme="dark"]:has(.backmatter-studio)
|
|
175
|
+
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
176
|
+
[data-no-ring]:focus,
|
|
164
177
|
html[data-theme="dark"]:has(.backmatter-studio)
|
|
165
178
|
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
166
179
|
[data-no-ring]:focus-visible {
|
|
167
180
|
outline: none;
|
|
181
|
+
outline-offset: 0;
|
|
182
|
+
box-shadow: none;
|
|
168
183
|
}
|
package/src/site-pointer.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// packages/studio/src/site-pointer.ts
|
|
2
2
|
// Hit-test a click on the host page and draw the armed pointer outline.
|
|
3
3
|
|
|
4
|
+
import { readElementOrigin, type ElementOrigin } from "./element-origin";
|
|
5
|
+
|
|
4
6
|
export const POINTER_QUOTE_MAX = 240;
|
|
5
7
|
|
|
6
8
|
export type PointerGrab = {
|
|
@@ -8,6 +10,14 @@ export type PointerGrab = {
|
|
|
8
10
|
blockType: string | null;
|
|
9
11
|
blockIndex: number | null;
|
|
10
12
|
quote: string;
|
|
13
|
+
} & ElementOrigin;
|
|
14
|
+
|
|
15
|
+
const EMPTY_ORIGIN: ElementOrigin = {
|
|
16
|
+
component: null,
|
|
17
|
+
file: null,
|
|
18
|
+
line: null,
|
|
19
|
+
column: null,
|
|
20
|
+
stack: [],
|
|
11
21
|
};
|
|
12
22
|
|
|
13
23
|
function quoteOf(target: Element): string {
|
|
@@ -29,9 +39,10 @@ export function grabAt(target: Element | null, bar: Element | null): PointerGrab
|
|
|
29
39
|
|
|
30
40
|
const marker = target.closest("[data-backmatter-block]");
|
|
31
41
|
const quote = quoteOf(target);
|
|
42
|
+
const origin = readElementOrigin(target) ?? EMPTY_ORIGIN;
|
|
32
43
|
if (!marker) {
|
|
33
|
-
if (!quote) return null;
|
|
34
|
-
return { blockId: null, blockType: null, blockIndex: null, quote };
|
|
44
|
+
if (!quote && !origin.component && !origin.file) return null;
|
|
45
|
+
return { blockId: null, blockType: null, blockIndex: null, quote, ...origin };
|
|
35
46
|
}
|
|
36
47
|
const indexRaw = marker.getAttribute("data-backmatter-index");
|
|
37
48
|
const blockIndex = indexRaw !== null && /^\d+$/.test(indexRaw) ? Number(indexRaw) : null;
|
|
@@ -40,6 +51,7 @@ export function grabAt(target: Element | null, bar: Element | null): PointerGrab
|
|
|
40
51
|
blockType: presentAttr(marker, "data-backmatter-type"),
|
|
41
52
|
blockIndex,
|
|
42
53
|
quote,
|
|
54
|
+
...origin,
|
|
43
55
|
};
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -104,10 +116,13 @@ export function installSitePointer(
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
function onClick(event: MouseEvent) {
|
|
107
|
-
if (!armed
|
|
119
|
+
if (!armed) return;
|
|
120
|
+
const raw = event.target instanceof Element ? event.target : null;
|
|
121
|
+
// The outline sits on the element. A click on it belongs to the element under it.
|
|
122
|
+
const target = raw?.closest("[data-backmatter-pointer]") ? current : raw;
|
|
123
|
+
if (!target || ignored(target)) return;
|
|
108
124
|
event.preventDefault();
|
|
109
125
|
event.stopPropagation();
|
|
110
|
-
const target = event.target instanceof Element ? event.target : null;
|
|
111
126
|
const grab = grabAt(target, options.bar());
|
|
112
127
|
if (grab) options.onGrab(grab, isOpenModifier(event));
|
|
113
128
|
}
|
|
@@ -174,6 +189,7 @@ function createPointerOutline(doc: Document): HTMLDivElement {
|
|
|
174
189
|
label.style.background = "#18181b";
|
|
175
190
|
label.style.color = "#ffffff";
|
|
176
191
|
label.style.font = "11px/1.4 ui-sans-serif, system-ui, sans-serif";
|
|
192
|
+
label.style.pointerEvents = "none";
|
|
177
193
|
label.style.display = "none";
|
|
178
194
|
el.appendChild(label);
|
|
179
195
|
doc.body.appendChild(el);
|
|
@@ -216,7 +232,10 @@ function positionPointerOutline(outline: HTMLDivElement, target: Element) {
|
|
|
216
232
|
outline.style.width = `${rect.width}px`;
|
|
217
233
|
outline.style.height = `${rect.height}px`;
|
|
218
234
|
const label = outline.firstElementChild as HTMLElement | null;
|
|
219
|
-
const name =
|
|
235
|
+
const name =
|
|
236
|
+
target.closest("[data-backmatter-block]")?.getAttribute("data-backmatter-type") ||
|
|
237
|
+
readElementOrigin(target)?.component ||
|
|
238
|
+
"";
|
|
220
239
|
if (!label) return;
|
|
221
240
|
if (!name) {
|
|
222
241
|
label.textContent = "";
|
package/src/studio-config.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type StudioConfig = {
|
|
|
14
14
|
dev: boolean;
|
|
15
15
|
/**
|
|
16
16
|
* The CMS API origin as the outside world reaches it, for URLs the Studio
|
|
17
|
-
* shows rather than fetches (the Content API
|
|
17
|
+
* shows rather than fetches (the Content API URL in General settings). A site Studio fetches
|
|
18
18
|
* through a relative proxy, so its host passes the real origin here.
|
|
19
19
|
* Defaults to `baseUrl`.
|
|
20
20
|
*/
|