@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
|
@@ -5,6 +5,7 @@ import { useOutletContext, useParams, useSearchParams, Link, useNavigate } from
|
|
|
5
5
|
import * as stylex from "@stylexjs/stylex";
|
|
6
6
|
import {
|
|
7
7
|
fetchDoc,
|
|
8
|
+
fetchDocs,
|
|
8
9
|
notifyTreeChanged,
|
|
9
10
|
type TreeChangeDetail,
|
|
10
11
|
createPreviewTicket,
|
|
@@ -17,8 +18,11 @@ import {
|
|
|
17
18
|
type SchemaPayload,
|
|
18
19
|
type SchemaField,
|
|
19
20
|
} from "../api";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
21
|
+
import { FieldControl } from "../fields/FieldControl";
|
|
22
|
+
import { loadRelationDocuments } from "../fields/load-relation-documents";
|
|
23
|
+
import { omitField } from "../fields/omit-field";
|
|
24
|
+
import { relationTargets } from "../fields/relation-targets";
|
|
25
|
+
import { useReplicaState } from "../replica/useReplica";
|
|
22
26
|
import { useSetEditorActions } from "../chrome/EditorActionsContext";
|
|
23
27
|
import {
|
|
24
28
|
DOC_CHANGED_EVENT,
|
|
@@ -46,6 +50,8 @@ import {
|
|
|
46
50
|
persistPreviewPct,
|
|
47
51
|
readPreviewPct,
|
|
48
52
|
} from "./preview-split";
|
|
53
|
+
import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
|
|
54
|
+
import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
|
|
49
55
|
import {
|
|
50
56
|
editorPreviewPath,
|
|
51
57
|
modelForType,
|
|
@@ -128,48 +134,6 @@ function emptyFrontmatter(model: SchemaPayload["models"][number] | undefined) {
|
|
|
128
134
|
return out;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
function JsonField({
|
|
132
|
-
value,
|
|
133
|
-
kind,
|
|
134
|
-
readOnly,
|
|
135
|
-
onValidChange,
|
|
136
|
-
}: {
|
|
137
|
-
value: unknown;
|
|
138
|
-
kind: "object" | "array";
|
|
139
|
-
readOnly?: boolean;
|
|
140
|
-
onValidChange: (next: unknown) => void;
|
|
141
|
-
}) {
|
|
142
|
-
const serialized = JSON.stringify(value ?? (kind === "array" ? [] : {}), null, 2);
|
|
143
|
-
const [draft, setDraft] = useState(serialized);
|
|
144
|
-
const [focused, setFocused] = useState(false);
|
|
145
|
-
const [invalid, setInvalid] = useState(false);
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (!focused) {
|
|
148
|
-
setDraft(serialized);
|
|
149
|
-
setInvalid(false);
|
|
150
|
-
}
|
|
151
|
-
}, [serialized, focused]);
|
|
152
|
-
return (
|
|
153
|
-
<Textarea
|
|
154
|
-
value={draft}
|
|
155
|
-
readOnly={readOnly}
|
|
156
|
-
aria-invalid={invalid || undefined}
|
|
157
|
-
onFocus={() => setFocused(true)}
|
|
158
|
-
onBlur={() => setFocused(false)}
|
|
159
|
-
onChange={(e) => {
|
|
160
|
-
const next = e.target.value;
|
|
161
|
-
setDraft(next);
|
|
162
|
-
try {
|
|
163
|
-
onValidChange(JSON.parse(next || (kind === "array" ? "[]" : "{}")));
|
|
164
|
-
setInvalid(false);
|
|
165
|
-
} catch {
|
|
166
|
-
setInvalid(true);
|
|
167
|
-
}
|
|
168
|
-
}}
|
|
169
|
-
/>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
137
|
/** Status-bar label for the doc's state against the branch HEAD. */
|
|
174
138
|
export function docStatusLabel(status: DocStatus | null): string | null {
|
|
175
139
|
if (status === null) return null;
|
|
@@ -200,7 +164,31 @@ export function DocEditorPage() {
|
|
|
200
164
|
const [docFormat, setDocFormat] = useState<"mdx" | "page">("mdx");
|
|
201
165
|
const [fieldKinds, setFieldKinds] = useState<Record<string, string>>({});
|
|
202
166
|
const [fieldDefs, setFieldDefs] = useState<Record<string, SchemaField>>({});
|
|
203
|
-
const
|
|
167
|
+
const replica = useReplicaState();
|
|
168
|
+
const objects = useMemo(
|
|
169
|
+
() => replica.checkout(workspace.id)?.schema?.payload.objects ?? [],
|
|
170
|
+
[replica, workspace.id],
|
|
171
|
+
);
|
|
172
|
+
const relationTargetTypes = useMemo(() => {
|
|
173
|
+
const targets = new Set<string>();
|
|
174
|
+
const groups: Record<string, SchemaField>[] = [];
|
|
175
|
+
if (model?.fields) groups.push(model.fields);
|
|
176
|
+
for (const block of blocks) {
|
|
177
|
+
if (block.fields) groups.push(block.fields);
|
|
178
|
+
}
|
|
179
|
+
for (const block of richtext) {
|
|
180
|
+
if (block.fields) groups.push(block.fields);
|
|
181
|
+
}
|
|
182
|
+
for (const fields of groups) {
|
|
183
|
+
for (const type of relationTargets(fields, models, objects)) {
|
|
184
|
+
targets.add(type);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [...targets].sort();
|
|
188
|
+
}, [model, blocks, richtext, models, objects]);
|
|
189
|
+
const [documentLists, setDocumentLists] = useState<
|
|
190
|
+
Record<string, Array<{ slug: string; frontmatter: Record<string, unknown> }>> | undefined
|
|
191
|
+
>(undefined);
|
|
204
192
|
const [parseFailed, setParseFailed] = useState(false);
|
|
205
193
|
const [error, setError] = useState<string | null>(null);
|
|
206
194
|
const [status, setStatus] = useState<string | null>(null);
|
|
@@ -214,6 +202,7 @@ export function DocEditorPage() {
|
|
|
214
202
|
const [previewOffer, setPreviewOffer] = useState<PreviewChannelRef | null>(null);
|
|
215
203
|
const [previewLive, setPreviewLive] = useState<"rendered" | "error" | null>(null);
|
|
216
204
|
const [previewPct, setPreviewPct] = useState(readPreviewPct);
|
|
205
|
+
const [previewSize, setPreviewSize] = useState(readPreviewSize);
|
|
217
206
|
const [dragging, setDragging] = useState(false);
|
|
218
207
|
const [editEpoch, setEditEpoch] = useState(0);
|
|
219
208
|
const [selection, setSelection] = useState<EditorSelection | null>(null);
|
|
@@ -302,6 +291,31 @@ export function DocEditorPage() {
|
|
|
302
291
|
};
|
|
303
292
|
}, [frameIdentity, previewPath, workspaceId, siteUrl, branch, tree]);
|
|
304
293
|
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (relationTargetTypes.length === 0) {
|
|
296
|
+
setDocumentLists({});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let cancelled = false;
|
|
300
|
+
setDocumentLists(undefined);
|
|
301
|
+
void loadRelationDocuments(
|
|
302
|
+
relationTargetTypes,
|
|
303
|
+
async () => (await fetchDocs(workspaceId)).docs,
|
|
304
|
+
async (relationType, relationSlug) => ({
|
|
305
|
+
frontmatter: (await fetchDoc(workspaceId, relationType, relationSlug)).doc.frontmatter,
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
.then((loaded) => {
|
|
309
|
+
if (!cancelled) setDocumentLists(loaded);
|
|
310
|
+
})
|
|
311
|
+
.catch(() => {
|
|
312
|
+
if (!cancelled) setDocumentLists({});
|
|
313
|
+
});
|
|
314
|
+
return () => {
|
|
315
|
+
cancelled = true;
|
|
316
|
+
};
|
|
317
|
+
}, [workspaceId, type, slug, relationTargetTypes.join("\0")]);
|
|
318
|
+
|
|
305
319
|
useEffect(() => {
|
|
306
320
|
setPreviewLoading(true);
|
|
307
321
|
}, [previewUrl]);
|
|
@@ -323,6 +337,7 @@ export function DocEditorPage() {
|
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
function onPreviewGrab(grab: PreviewGrabMessage) {
|
|
340
|
+
if (typeof grab.blockId !== "string" || !grab.blockId) return;
|
|
326
341
|
const ids = blocksFromFrontmatter(latest.current.frontmatter[blocksFieldRef.current]).map(
|
|
327
342
|
(block) => block.id,
|
|
328
343
|
);
|
|
@@ -330,7 +345,13 @@ export function DocEditorPage() {
|
|
|
330
345
|
grabRef: grab.ref,
|
|
331
346
|
openRef: channelRef.current,
|
|
332
347
|
blockIds: ids,
|
|
333
|
-
grab
|
|
348
|
+
grab: {
|
|
349
|
+
blockId: grab.blockId,
|
|
350
|
+
blockType: grab.blockType,
|
|
351
|
+
blockIndex: grab.blockIndex,
|
|
352
|
+
quote: grab.quote,
|
|
353
|
+
shift: grab.shift,
|
|
354
|
+
},
|
|
334
355
|
});
|
|
335
356
|
if (decision.action === "add") {
|
|
336
357
|
addPointer({
|
|
@@ -467,6 +488,11 @@ export function DocEditorPage() {
|
|
|
467
488
|
setPreviewKey((k) => k + 1);
|
|
468
489
|
}
|
|
469
490
|
|
|
491
|
+
function choosePreviewSize(size: PreviewSize) {
|
|
492
|
+
setPreviewSize(size);
|
|
493
|
+
persistPreviewSize(size);
|
|
494
|
+
}
|
|
495
|
+
|
|
470
496
|
function updatePreviewPctFromPointer(clientX: number, clientY: number) {
|
|
471
497
|
const split = splitRef.current;
|
|
472
498
|
if (!split) return;
|
|
@@ -901,6 +927,7 @@ export function DocEditorPage() {
|
|
|
901
927
|
</button>
|
|
902
928
|
) : null}
|
|
903
929
|
</div>
|
|
930
|
+
<PreviewSizeSwitch size={previewSize} onSize={choosePreviewSize} />
|
|
904
931
|
<span
|
|
905
932
|
{...stylex.props(
|
|
906
933
|
styles.badge,
|
|
@@ -911,20 +938,22 @@ export function DocEditorPage() {
|
|
|
911
938
|
</span>
|
|
912
939
|
</div>
|
|
913
940
|
{previewUrl ? (
|
|
914
|
-
<
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
941
|
+
<PreviewViewport size={previewSize} dragging={dragging}>
|
|
942
|
+
<iframe
|
|
943
|
+
key={`${previewKey}-${previewUrl}`}
|
|
944
|
+
ref={previewFrameRef}
|
|
945
|
+
title="Live preview"
|
|
946
|
+
src={previewUrl}
|
|
947
|
+
onLoad={() => {
|
|
948
|
+
setPreviewLoading(false);
|
|
949
|
+
setPreviewFrame((n) => n + 1);
|
|
950
|
+
}}
|
|
951
|
+
{...stylex.props(
|
|
952
|
+
styles.iframe,
|
|
953
|
+
dragging && styles.iframeDragging,
|
|
954
|
+
)}
|
|
955
|
+
/>
|
|
956
|
+
</PreviewViewport>
|
|
928
957
|
) : !siteUrl ? (
|
|
929
958
|
<div {...stylex.props(styles.iframe, styles.previewEmpty)}>
|
|
930
959
|
<Text as="p" size="sm" weight="medium">
|
|
@@ -1085,55 +1114,33 @@ export function DocEditorPage() {
|
|
|
1085
1114
|
</Text>
|
|
1086
1115
|
) : (
|
|
1087
1116
|
detailNames.map((name) => {
|
|
1088
|
-
const
|
|
1089
|
-
const composite = kind === "object" || kind === "array";
|
|
1090
|
-
const CustomInput = resolveFieldInput(studio, fieldDefs[name]);
|
|
1117
|
+
const field = fieldDefs[name]!;
|
|
1091
1118
|
return (
|
|
1092
1119
|
<Field key={name} label={fieldLabel(name)}>
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
value={frontmatter[name]}
|
|
1118
|
-
readOnly={aiEditing || parseFailed}
|
|
1119
|
-
onValidChange={(next) => {
|
|
1120
|
-
markEdited();
|
|
1121
|
-
setFrontmatter((prev) => ({ ...prev, [name]: next }));
|
|
1122
|
-
}}
|
|
1123
|
-
/>
|
|
1124
|
-
) : (
|
|
1125
|
-
<Input
|
|
1126
|
-
value={String(frontmatter[name] ?? "")}
|
|
1127
|
-
readOnly={aiEditing || parseFailed}
|
|
1128
|
-
onChange={(e) => {
|
|
1129
|
-
markEdited();
|
|
1130
|
-
setFrontmatter((prev) => ({
|
|
1131
|
-
...prev,
|
|
1132
|
-
[name]: e.target.value,
|
|
1133
|
-
}));
|
|
1134
|
-
}}
|
|
1135
|
-
/>
|
|
1136
|
-
)}
|
|
1120
|
+
<FieldControl
|
|
1121
|
+
name={name}
|
|
1122
|
+
field={field}
|
|
1123
|
+
value={frontmatter[name]}
|
|
1124
|
+
models={models}
|
|
1125
|
+
objects={objects}
|
|
1126
|
+
documentLists={documentLists}
|
|
1127
|
+
readOnly={aiEditing || parseFailed}
|
|
1128
|
+
blocksEditor={({ value, readOnly: blocksReadOnly, onChange: onBlocksChange }) => (
|
|
1129
|
+
<BlocksList
|
|
1130
|
+
blocks={blocksFromFrontmatter(value)}
|
|
1131
|
+
catalog={blocks}
|
|
1132
|
+
models={models}
|
|
1133
|
+
objects={objects}
|
|
1134
|
+
documentLists={documentLists}
|
|
1135
|
+
readOnly={blocksReadOnly}
|
|
1136
|
+
onChange={onBlocksChange}
|
|
1137
|
+
/>
|
|
1138
|
+
)}
|
|
1139
|
+
onChange={(next) => {
|
|
1140
|
+
markEdited();
|
|
1141
|
+
setFrontmatter((prev) => omitField(prev, name, next));
|
|
1142
|
+
}}
|
|
1143
|
+
/>
|
|
1137
1144
|
</Field>
|
|
1138
1145
|
);
|
|
1139
1146
|
})
|
|
@@ -1142,6 +1149,9 @@ export function DocEditorPage() {
|
|
|
1142
1149
|
<BlocksList
|
|
1143
1150
|
blocks={blocksFromFrontmatter(frontmatter[blocksField])}
|
|
1144
1151
|
catalog={blocks}
|
|
1152
|
+
models={models}
|
|
1153
|
+
objects={objects}
|
|
1154
|
+
documentLists={documentLists}
|
|
1145
1155
|
readOnly={aiEditing || parseFailed}
|
|
1146
1156
|
focusBlock={blockFocus}
|
|
1147
1157
|
onChange={(next) => {
|
|
@@ -1153,6 +1163,9 @@ export function DocEditorPage() {
|
|
|
1153
1163
|
<BodyField
|
|
1154
1164
|
value={body}
|
|
1155
1165
|
catalog={richtext}
|
|
1166
|
+
models={models}
|
|
1167
|
+
objects={objects}
|
|
1168
|
+
documentLists={documentLists}
|
|
1156
1169
|
readOnly={aiEditing || parseFailed}
|
|
1157
1170
|
onChange={(value) => {
|
|
1158
1171
|
markEdited();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// packages/studio/src/pages/PreviewSize.tsx
|
|
2
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
|
+
import * as stylex from "@stylexjs/stylex";
|
|
4
|
+
import {
|
|
5
|
+
IconImac,
|
|
6
|
+
IconPhone,
|
|
7
|
+
IconTablet,
|
|
8
|
+
} from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
|
|
9
|
+
import { colors, radius } from "../styles/tokens.stylex";
|
|
10
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
11
|
+
import {
|
|
12
|
+
previewFrameWidth,
|
|
13
|
+
type PreviewSize,
|
|
14
|
+
} from "./preview-size";
|
|
15
|
+
|
|
16
|
+
const OPTIONS: readonly { size: PreviewSize; label: string; icon: typeof IconImac }[] = [
|
|
17
|
+
{ size: "desktop", label: "Desktop", icon: IconImac },
|
|
18
|
+
{ size: "tablet", label: "Tablet", icon: IconTablet },
|
|
19
|
+
{ size: "phone", label: "Phone", icon: IconPhone },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function PreviewSizeSwitch({
|
|
23
|
+
size,
|
|
24
|
+
onSize,
|
|
25
|
+
disabled = false,
|
|
26
|
+
}: {
|
|
27
|
+
size: PreviewSize;
|
|
28
|
+
onSize: (size: PreviewSize) => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
32
|
+
if (disabled) return;
|
|
33
|
+
const index = OPTIONS.findIndex((option) => option.size === size);
|
|
34
|
+
const next =
|
|
35
|
+
event.key === "ArrowRight" ? OPTIONS[(index + 1) % OPTIONS.length]
|
|
36
|
+
: event.key === "ArrowLeft" ? OPTIONS[(index - 1 + OPTIONS.length) % OPTIONS.length]
|
|
37
|
+
: null;
|
|
38
|
+
if (!next) return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
onSize(next.size);
|
|
41
|
+
event.currentTarget.querySelector<HTMLButtonElement>(`[data-preview-size="${next.size}"]`)?.focus();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
role="radiogroup"
|
|
47
|
+
aria-label="Preview size"
|
|
48
|
+
onKeyDown={onKeyDown}
|
|
49
|
+
{...stylex.props(styles.group)}
|
|
50
|
+
>
|
|
51
|
+
{OPTIONS.map((option) => {
|
|
52
|
+
const selected = option.size === size;
|
|
53
|
+
const Icon = option.icon;
|
|
54
|
+
return (
|
|
55
|
+
<Tooltip key={option.size} label={option.label}>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
role="radio"
|
|
59
|
+
data-preview-size={option.size}
|
|
60
|
+
aria-label={option.label}
|
|
61
|
+
aria-checked={selected}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
tabIndex={selected ? 0 : -1}
|
|
64
|
+
onClick={() => onSize(option.size)}
|
|
65
|
+
{...stylex.props(
|
|
66
|
+
styles.option,
|
|
67
|
+
selected && styles.optionOn,
|
|
68
|
+
disabled && styles.optionDisabled,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<Icon size={14} ariaHidden />
|
|
72
|
+
</button>
|
|
73
|
+
</Tooltip>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Centers a fixed-width preview and scrolls sideways when the pane is narrower. */
|
|
81
|
+
export function PreviewViewport({
|
|
82
|
+
size,
|
|
83
|
+
dragging = false,
|
|
84
|
+
children,
|
|
85
|
+
}: {
|
|
86
|
+
size: PreviewSize;
|
|
87
|
+
dragging?: boolean;
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
}) {
|
|
90
|
+
const width = previewFrameWidth(size);
|
|
91
|
+
const viewport = stylex.props(styles.viewport, dragging && styles.dragging);
|
|
92
|
+
const slot = stylex.props(styles.slot, width !== null && styles.slotFixed);
|
|
93
|
+
return (
|
|
94
|
+
<div className={viewport.className} style={viewport.style}>
|
|
95
|
+
<div
|
|
96
|
+
className={slot.className}
|
|
97
|
+
style={width === null ? slot.style : { ...slot.style, width }}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hover = "@media (hover: hover)";
|
|
106
|
+
|
|
107
|
+
const styles = stylex.create({
|
|
108
|
+
group: {
|
|
109
|
+
display: "inline-flex",
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
gap: "2px",
|
|
112
|
+
padding: "2px",
|
|
113
|
+
borderRadius: radius.md,
|
|
114
|
+
backgroundColor: colors.bgPanelInset,
|
|
115
|
+
},
|
|
116
|
+
option: {
|
|
117
|
+
display: "inline-flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
width: "28px",
|
|
121
|
+
height: "22px",
|
|
122
|
+
padding: 0,
|
|
123
|
+
borderWidth: 0,
|
|
124
|
+
borderRadius: radius.sm,
|
|
125
|
+
color: colors.textMuted,
|
|
126
|
+
backgroundColor: "transparent",
|
|
127
|
+
cursor: "pointer",
|
|
128
|
+
[hover]: {
|
|
129
|
+
":hover:not(:disabled)": {
|
|
130
|
+
color: colors.text,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
":focus-visible": {
|
|
134
|
+
outlineWidth: 2,
|
|
135
|
+
outlineStyle: "solid",
|
|
136
|
+
outlineColor: colors.focus,
|
|
137
|
+
outlineOffset: 1,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
optionOn: {
|
|
141
|
+
color: colors.text,
|
|
142
|
+
backgroundColor: colors.bgSurface,
|
|
143
|
+
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
|
144
|
+
},
|
|
145
|
+
optionDisabled: {
|
|
146
|
+
cursor: "default",
|
|
147
|
+
color: colors.border,
|
|
148
|
+
},
|
|
149
|
+
viewport: {
|
|
150
|
+
display: "flex",
|
|
151
|
+
flex: 1,
|
|
152
|
+
alignItems: "stretch",
|
|
153
|
+
justifyContent: "center",
|
|
154
|
+
minWidth: 0,
|
|
155
|
+
minHeight: 0,
|
|
156
|
+
overflowX: "auto",
|
|
157
|
+
overflowY: "hidden",
|
|
158
|
+
},
|
|
159
|
+
dragging: {
|
|
160
|
+
pointerEvents: "none",
|
|
161
|
+
},
|
|
162
|
+
slot: {
|
|
163
|
+
position: "relative",
|
|
164
|
+
display: "flex",
|
|
165
|
+
flexDirection: "column",
|
|
166
|
+
flexGrow: 1,
|
|
167
|
+
flexShrink: 1,
|
|
168
|
+
flexBasis: "0%",
|
|
169
|
+
minWidth: 0,
|
|
170
|
+
minHeight: 0,
|
|
171
|
+
backgroundColor: colors.bgSurface,
|
|
172
|
+
},
|
|
173
|
+
slotFixed: {
|
|
174
|
+
flexGrow: 0,
|
|
175
|
+
flexShrink: 0,
|
|
176
|
+
flexBasis: "auto",
|
|
177
|
+
outlineWidth: 1,
|
|
178
|
+
outlineStyle: "solid",
|
|
179
|
+
outlineColor: colors.border,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
11
|
import * as stylex from "@stylexjs/stylex";
|
|
12
|
-
import type { BlockSnapshot } from "../api";
|
|
12
|
+
import type { BlockSnapshot, SchemaField } from "../api";
|
|
13
13
|
import { contentBlockFields, contentBlockLabel, contentBlockProps } from "../content-blocks";
|
|
14
14
|
import { colors, radius, space } from "../styles/tokens.stylex";
|
|
15
15
|
import {
|
|
@@ -40,16 +40,28 @@ export type RenderedBodyHandle = {
|
|
|
40
40
|
|
|
41
41
|
const BODY_HOST = "body";
|
|
42
42
|
|
|
43
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
44
|
+
type RelationDocumentLists = Record<
|
|
45
|
+
string,
|
|
46
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
47
|
+
>;
|
|
48
|
+
|
|
43
49
|
export const RenderedBody = forwardRef<
|
|
44
50
|
RenderedBodyHandle,
|
|
45
51
|
{
|
|
46
52
|
value: string;
|
|
47
53
|
catalog?: Array<BlockSnapshot & { label?: string }>;
|
|
54
|
+
models: ModelRef[];
|
|
55
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
56
|
+
documentLists?: RelationDocumentLists;
|
|
48
57
|
readOnly?: boolean;
|
|
49
58
|
labelledBy: string;
|
|
50
59
|
onChange: (value: string) => void;
|
|
51
60
|
}
|
|
52
|
-
>(function RenderedBody(
|
|
61
|
+
>(function RenderedBody(
|
|
62
|
+
{ value, catalog = [], models, objects = [], documentLists, readOnly, labelledBy, onChange },
|
|
63
|
+
ref,
|
|
64
|
+
) {
|
|
53
65
|
const local = useRef(value);
|
|
54
66
|
const hosts = useRef(new Map<string, HTMLElement>());
|
|
55
67
|
const linking = useRef(false);
|
|
@@ -218,6 +230,9 @@ export const RenderedBody = forwardRef<
|
|
|
218
230
|
title={contentBlockLabel(settings.def.name)}
|
|
219
231
|
fields={contentBlockFields(settings.def)}
|
|
220
232
|
value={contentBlockProps(settings.block.markdown, settings.def.name)}
|
|
233
|
+
models={models}
|
|
234
|
+
objects={objects}
|
|
235
|
+
documentLists={documentLists}
|
|
221
236
|
readOnly={readOnly}
|
|
222
237
|
onOpenChange={(open) => {
|
|
223
238
|
if (!open) setSettingsId(null);
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
rotateHostToken,
|
|
8
8
|
rotatePreviewToken,
|
|
9
9
|
} from "../api";
|
|
10
|
+
import { publicContentApiUrl } from "../chrome/content-api-url";
|
|
11
|
+
import { useReplicaState } from "../replica/useReplica";
|
|
10
12
|
import { hostEnvSnippet } from "./host-env";
|
|
11
13
|
import { isStudioDev, publicApiUrl } from "../studio-config";
|
|
12
14
|
import { revalidateWebhookUrl } from "./preview-url";
|
|
@@ -24,6 +26,7 @@ export function SettingsGeneralPage() {
|
|
|
24
26
|
const [previewTokenOnce, setPreviewTokenOnce] = useState<string | null>(null);
|
|
25
27
|
const [hostTokenOnce, setHostTokenOnce] = useState<string | null>(null);
|
|
26
28
|
const [copiedId, setCopiedId] = useState(false);
|
|
29
|
+
const [copiedApi, setCopiedApi] = useState(false);
|
|
27
30
|
const [copiedEnv, setCopiedEnv] = useState(false);
|
|
28
31
|
const [siteUrl, setSiteUrl] = useState(workspace.siteUrls?.[0] ?? "");
|
|
29
32
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -34,13 +37,25 @@ export function SettingsGeneralPage() {
|
|
|
34
37
|
const webhookUrl = revalidateWebhookUrl(siteUrl);
|
|
35
38
|
|
|
36
39
|
useEffect(() => {
|
|
37
|
-
if (!copiedId && !copiedEnv) return;
|
|
40
|
+
if (!copiedId && !copiedApi && !copiedEnv) return;
|
|
38
41
|
const id = window.setTimeout(() => {
|
|
39
42
|
setCopiedId(false);
|
|
43
|
+
setCopiedApi(false);
|
|
40
44
|
setCopiedEnv(false);
|
|
41
45
|
}, 1500);
|
|
42
46
|
return () => window.clearTimeout(id);
|
|
43
|
-
}, [copiedId, copiedEnv]);
|
|
47
|
+
}, [copiedId, copiedApi, copiedEnv]);
|
|
48
|
+
|
|
49
|
+
const git = useReplicaState().git(workspace.id);
|
|
50
|
+
const docsUrl = publicContentApiUrl({
|
|
51
|
+
apiUrl: publicApiUrl(),
|
|
52
|
+
workspaceId: workspace.id,
|
|
53
|
+
branch: git?.currentBranch,
|
|
54
|
+
defaultBranch: git?.defaultBranch,
|
|
55
|
+
});
|
|
56
|
+
const branchLocked = Boolean(
|
|
57
|
+
git?.currentBranch && git.defaultBranch && git.currentBranch !== git.defaultBranch,
|
|
58
|
+
);
|
|
44
59
|
|
|
45
60
|
const envSnippet = hostEnvSnippet({
|
|
46
61
|
workspaceId: workspace.id,
|
|
@@ -197,6 +212,38 @@ export function SettingsGeneralPage() {
|
|
|
197
212
|
backmatter.config.ts workspaceId · public URLs use this id, not the slug
|
|
198
213
|
</p>
|
|
199
214
|
</Panel>
|
|
215
|
+
<Panel>
|
|
216
|
+
<Text as="h2" size="lg" weight="medium">
|
|
217
|
+
Content API
|
|
218
|
+
</Text>
|
|
219
|
+
<div {...stylex.props(styles.idRow)}>
|
|
220
|
+
<a
|
|
221
|
+
href={docsUrl}
|
|
222
|
+
target="_blank"
|
|
223
|
+
rel="noreferrer"
|
|
224
|
+
{...stylex.props(styles.pre, styles.idPre, styles.apiLink)}
|
|
225
|
+
>
|
|
226
|
+
{docsUrl}
|
|
227
|
+
</a>
|
|
228
|
+
<Button
|
|
229
|
+
variant="secondary"
|
|
230
|
+
aria-label={copiedApi ? "Copied content API URL" : "Copy content API URL"}
|
|
231
|
+
onClick={() => {
|
|
232
|
+
void navigator.clipboard.writeText(docsUrl).then(
|
|
233
|
+
() => setCopiedApi(true),
|
|
234
|
+
() => setCopiedApi(false),
|
|
235
|
+
);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{copiedApi ? "Copied" : "Copy"}
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
<p {...stylex.props(styles.caption)}>
|
|
242
|
+
{branchLocked
|
|
243
|
+
? "This branch needs the content token below."
|
|
244
|
+
: "Default branch is public — no token. URLs use the workspace id."}
|
|
245
|
+
</p>
|
|
246
|
+
</Panel>
|
|
200
247
|
<Panel>
|
|
201
248
|
<Text as="h2" size="lg" weight="medium">
|
|
202
249
|
Host environment
|
|
@@ -358,6 +405,8 @@ export function relativeTime(iso: string, now = Date.now()): string {
|
|
|
358
405
|
return `${Math.round(hours / 24)}d ago`;
|
|
359
406
|
}
|
|
360
407
|
|
|
408
|
+
const hover = "@media (hover: hover)";
|
|
409
|
+
|
|
361
410
|
const styles = stylex.create({
|
|
362
411
|
block: {
|
|
363
412
|
marginTop: space.md,
|
|
@@ -379,6 +428,22 @@ const styles = stylex.create({
|
|
|
379
428
|
flex: 1,
|
|
380
429
|
minWidth: 0,
|
|
381
430
|
},
|
|
431
|
+
apiLink: {
|
|
432
|
+
display: "block",
|
|
433
|
+
color: colors.text,
|
|
434
|
+
textDecoration: "none",
|
|
435
|
+
[hover]: {
|
|
436
|
+
":hover": {
|
|
437
|
+
textDecoration: "underline",
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
":focus-visible": {
|
|
441
|
+
outlineWidth: 2,
|
|
442
|
+
outlineStyle: "solid",
|
|
443
|
+
outlineColor: colors.focus,
|
|
444
|
+
outlineOffset: 2,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
382
447
|
caption: {
|
|
383
448
|
marginTop: space.sm,
|
|
384
449
|
marginBottom: 0,
|