@backmatter/studio 0.0.11 → 0.0.13

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.
Files changed (34) hide show
  1. package/dist/chunks/{UiKitApp-Cu0CS1NU.js → UiKitApp-ZZXQt3zy.js} +29 -29
  2. package/dist/chunks/{WorkspaceRoutes-Dzpaj8ov.js → WorkspaceRoutes-CCDPtLla.js} +22396 -22185
  3. package/dist/chunks/{dashboard-kWmpeuAP.js → dashboard-Dw0zfTXg.js} +35 -35
  4. package/dist/chunks/{index-Bryt_VJQ.js → index-DhZGCAru.js} +1 -1
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +199 -193
  7. package/dist/site-widget.css +9 -1
  8. package/dist/site-widget.js +6060 -5936
  9. package/dist/studio.css +5 -25
  10. package/package.json +6 -2
  11. package/src/chat/ChatProvider.tsx +13 -2
  12. package/src/chat/Composer.tsx +2 -1
  13. package/src/chat/PointerChip.tsx +15 -2
  14. package/src/chat/chat-helpers.ts +6 -0
  15. package/src/chat/pointer-arm.ts +3 -0
  16. package/src/chrome/WorkspaceStatusBar.tsx +57 -19
  17. package/src/chrome/commit-error.ts +82 -0
  18. package/src/chrome/commit-failure.tsx +111 -0
  19. package/src/install-preview-pointer.ts +116 -0
  20. package/src/pages/BlocksList.tsx +17 -2
  21. package/src/pages/DocEditorPage.tsx +95 -1
  22. package/src/pages/EmbedPublishPage.tsx +27 -24
  23. package/src/pages/RenderedBody.tsx +141 -322
  24. package/src/pages/document-paint.ts +79 -0
  25. package/src/pages/editor-grab.ts +39 -0
  26. package/src/pages/html-to-markdown.ts +26 -0
  27. package/src/pages/preview-bridge.ts +8 -1
  28. package/src/pages/selection-format.ts +5 -2
  29. package/src/preview-highlight.ts +16 -0
  30. package/src/preview-pointer-host.ts +30 -0
  31. package/src/shell-resets.css +13 -0
  32. package/src/site-changes.ts +46 -0
  33. package/src/site-pointer.ts +39 -6
  34. package/src/site-widget.tsx +66 -23
@@ -62,7 +62,16 @@ import { blocksFromFrontmatter } from "./blocks-list";
62
62
  import type { WorkspaceOutletContext } from "../workspace/types";
63
63
  import { contentDocPath, defaultLocaleOf, settingsPath } from "../workspace-path";
64
64
  import { bindPreviewEditor, previewFrameTransport } from "./preview-bridge";
65
- import { createWindowPreviewTransport, type PreviewChannelRef } from "@backmatter/core/preview";
65
+ import { editorGrabAction } from "./editor-grab";
66
+ import { useChatUI } from "../chat/ChatProvider";
67
+ import { POINTER_ARM_EVENT } from "../chat/pointer-arm";
68
+ import { registerPreviewHighlight } from "../preview-highlight";
69
+ import {
70
+ createWindowPreviewTransport,
71
+ samePreviewRef,
72
+ type PreviewChannelRef,
73
+ type PreviewGrabMessage,
74
+ } from "@backmatter/core/preview";
66
75
 
67
76
  const AUTOSAVE_MS = 400;
68
77
  const hover = "@media (hover: hover)";
@@ -215,6 +224,9 @@ export function DocEditorPage() {
215
224
  const splitRef = useRef<HTMLDivElement>(null);
216
225
  const previewFrameRef = useRef<HTMLIFrameElement>(null);
217
226
  const previewSession = useRef<ReturnType<typeof bindPreviewEditor> | null>(null);
227
+ const { addPointer, setPreviewPointer, setPointerHighlight } = useChatUI();
228
+ const [blockFocus, setBlockFocus] = useState<{ id: string; nonce: number } | null>(null);
229
+ const blockFocusNonce = useRef(0);
218
230
  const navigate = useNavigate();
219
231
  const wide = useWideLayout();
220
232
  const { setStatus: setEditorStatus, clear: clearEditorStatus } =
@@ -254,6 +266,8 @@ export function DocEditorPage() {
254
266
  const detailNames = fieldNames.filter((name) => fieldKinds[name] !== "blocks");
255
267
  const blocksField =
256
268
  fieldNames.find((name) => fieldKinds[name] === "blocks") ?? "blocks";
269
+ const blocksFieldRef = useRef(blocksField);
270
+ blocksFieldRef.current = blocksField;
257
271
  const siteUrl = primarySiteUrl(workspace.siteUrls ?? []);
258
272
  const liveUrl = productionDocUrl(siteUrl, modelForType(models, type), locale, slug);
259
273
  const previewPath = editorPreviewPath(
@@ -307,6 +321,57 @@ export function DocEditorPage() {
307
321
  setPreviewLive(null);
308
322
  }, [previewUrl]);
309
323
 
324
+ function onPreviewHello(hello: PreviewChannelRef, capabilities?: string[]) {
325
+ const canPoint =
326
+ samePreviewRef(hello, channelRef.current) && (capabilities?.includes("point") ?? false);
327
+ setPreviewPointer(canPoint);
328
+ window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on: false } }));
329
+ }
330
+
331
+ function onPreviewGrab(grab: PreviewGrabMessage) {
332
+ const ids = blocksFromFrontmatter(latest.current.frontmatter[blocksFieldRef.current]).map(
333
+ (block) => block.id,
334
+ );
335
+ const decision = editorGrabAction({
336
+ grabRef: grab.ref,
337
+ openRef: channelRef.current,
338
+ blockIds: ids,
339
+ grab,
340
+ });
341
+ if (decision.action === "add") {
342
+ addPointer({
343
+ locale: channelRef.current.locale,
344
+ type: channelRef.current.type,
345
+ slug: channelRef.current.slug,
346
+ blockId: decision.blockId,
347
+ blockType: decision.blockType,
348
+ blockIndex: decision.blockIndex,
349
+ quote: decision.quote,
350
+ });
351
+ }
352
+ if (decision.action === "open") {
353
+ blockFocusNonce.current += 1;
354
+ setBlockFocus({ id: decision.blockId, nonce: blockFocusNonce.current });
355
+ }
356
+ }
357
+
358
+ function onPreviewPoint(on: boolean) {
359
+ if (!on) {
360
+ window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on: false } }));
361
+ }
362
+ }
363
+
364
+ const previewHandlers = useRef({
365
+ hello: onPreviewHello,
366
+ grab: onPreviewGrab,
367
+ point: onPreviewPoint,
368
+ });
369
+ previewHandlers.current = {
370
+ hello: onPreviewHello,
371
+ grab: onPreviewGrab,
372
+ point: onPreviewPoint,
373
+ };
374
+
310
375
  useEffect(() => {
311
376
  const frame = previewFrameRef.current;
312
377
  if (!frame || !siteUrl) return;
@@ -323,6 +388,9 @@ export function DocEditorPage() {
323
388
  ref: () => channelRef.current,
324
389
  onOffer: setPreviewOffer,
325
390
  onStatus: setPreviewLive,
391
+ onHello: (hello, capabilities) => previewHandlers.current.hello(hello, capabilities),
392
+ onGrab: (grab) => previewHandlers.current.grab(grab),
393
+ onPoint: (on) => previewHandlers.current.point(on),
326
394
  });
327
395
  previewSession.current = session;
328
396
  if (editEpochRef.current > 0) session.setDoc(latest.current);
@@ -352,6 +420,9 @@ export function DocEditorPage() {
352
420
  ref: () => channelRef.current,
353
421
  onOffer: setPreviewOffer,
354
422
  onStatus: setPreviewLive,
423
+ onHello: (hello, capabilities) => previewHandlers.current.hello(hello, capabilities),
424
+ onGrab: (grab) => previewHandlers.current.grab(grab),
425
+ onPoint: (on) => previewHandlers.current.point(on),
355
426
  });
356
427
  previewSession.current = session;
357
428
  if (editEpochRef.current > 0) session.setDoc(latest.current);
@@ -371,6 +442,28 @@ export function DocEditorPage() {
371
442
  };
372
443
  }, [embed]);
373
444
 
445
+ useEffect(() => {
446
+ function onArm(event: Event) {
447
+ const on = (event as CustomEvent<{ on?: boolean }>).detail?.on === true;
448
+ previewSession.current?.point(on);
449
+ }
450
+ window.addEventListener(POINTER_ARM_EVENT, onArm);
451
+ return () => window.removeEventListener(POINTER_ARM_EVENT, onArm);
452
+ }, []);
453
+
454
+ useEffect(() => {
455
+ setPointerHighlight("channel");
456
+ const stop = registerPreviewHighlight((blockId) => {
457
+ previewSession.current?.highlight(blockId);
458
+ });
459
+ return () => {
460
+ stop();
461
+ setPointerHighlight("local");
462
+ setPreviewPointer(false);
463
+ previewSession.current?.point(false);
464
+ };
465
+ }, [setPointerHighlight, setPreviewPointer]);
466
+
374
467
  useEffect(() => {
375
468
  if (editEpoch === 0) return;
376
469
  previewSession.current?.setDoc(latest.current);
@@ -1066,6 +1159,7 @@ export function DocEditorPage() {
1066
1159
  blocks={blocksFromFrontmatter(frontmatter[blocksField])}
1067
1160
  catalog={blocks}
1068
1161
  readOnly={aiEditing || parseFailed}
1162
+ focusBlock={blockFocus}
1069
1163
  onChange={(next) => {
1070
1164
  markEdited();
1071
1165
  setFrontmatter((prev) => ({ ...prev, [blocksField]: next }));
@@ -12,12 +12,17 @@ import {
12
12
  notifyTreeChanged,
13
13
  publishCommittedFiles,
14
14
  resolveDoc,
15
- type ApiError,
16
15
  type ChangeStatus,
17
16
  type GitChange,
18
17
  type GitState,
19
18
  } from "../api";
20
19
  import { statusBarAction } from "../chrome/status-bar-action";
20
+ import {
21
+ explainCommitError,
22
+ problemsForRef,
23
+ type CommitErrorView,
24
+ } from "../chrome/commit-error";
25
+ import { CommitFailureNotice } from "../chrome/commit-failure";
21
26
  import { colors, radius, space, type } from "../styles/tokens.stylex";
22
27
  import { popup } from "../ui/popup";
23
28
  import { canPublish } from "../workspace/permissions";
@@ -33,18 +38,6 @@ function parentOrigin(): string {
33
38
  }
34
39
  }
35
40
 
36
- function formatCommitError(err: unknown): string {
37
- if (!(err instanceof Error)) return "Commit failed";
38
- const apiErr = err as ApiError;
39
- if (apiErr.code === "conflict") return "Resolve conflicts before committing";
40
- if (apiErr.code === "nothing_to_commit") return "Nothing to commit";
41
- if (apiErr.code === "validation") {
42
- return err.message.replace(/^validation:\s*/, "Validation failed: ");
43
- }
44
- if (apiErr.code === "github_not_bound") return "Connect GitHub first";
45
- return err.message;
46
- }
47
-
48
41
  function changeKey(doc: GitChange) {
49
42
  return `${doc.locale}/${doc.type}/${doc.slug}`;
50
43
  }
@@ -72,6 +65,7 @@ export function EmbedPublishPage({
72
65
  const [role, setRole] = useState("viewer");
73
66
  const [message, setMessage] = useState("");
74
67
  const [error, setError] = useState<string | null>(null);
68
+ const [commitFailure, setCommitFailure] = useState<CommitErrorView | null>(null);
75
69
  const [committing, setCommitting] = useState(false);
76
70
  const [resolvingKey, setResolvingKey] = useState<string | null>(null);
77
71
 
@@ -157,6 +151,7 @@ export function EmbedPublishPage({
157
151
  if (committing || action !== "commit" || changes.length === 0) return;
158
152
  setCommitting(true);
159
153
  setError(null);
154
+ setCommitFailure(null);
160
155
  try {
161
156
  const result = await commitWorkspace(workspaceId, message);
162
157
  setMessage("");
@@ -168,7 +163,7 @@ export function EmbedPublishPage({
168
163
  }
169
164
  setGit(await fetchGit(workspaceId));
170
165
  } catch (err) {
171
- setError(formatCommitError(err));
166
+ setCommitFailure(explainCommitError(err));
172
167
  } finally {
173
168
  setCommitting(false);
174
169
  }
@@ -225,18 +220,23 @@ export function EmbedPublishPage({
225
220
  ? "Nothing to publish"
226
221
  : `${changes.length} ${changes.length === 1 ? "file" : "files"} → ${branch}`}
227
222
  </p>
223
+ {commitFailure ? <CommitFailureNotice failure={commitFailure} /> : null}
228
224
  {changes.length > 0 ? (
229
225
  <ul {...stylex.props(styles.files)}>
230
- {changes.map((doc) => (
231
- <li key={changeKey(doc)}>
232
- <div {...stylex.props(styles.file)}>
233
- <span {...stylex.props(styles.fileName)}>
234
- {doc.type}/{doc.slug}
235
- </span>
236
- <span {...stylex.props(popup.itemMeta)}>{STATUS_LABEL[doc.status]}</span>
237
- </div>
238
- </li>
239
- ))}
226
+ {changes.map((doc) => {
227
+ const invalid =
228
+ problemsForRef(commitFailure, `${doc.type}/${doc.slug}`).length > 0;
229
+ return (
230
+ <li key={changeKey(doc)}>
231
+ <div {...stylex.props(styles.file)}>
232
+ <span {...stylex.props(styles.fileName, invalid && styles.invalidName)}>
233
+ {doc.type}/{doc.slug}
234
+ </span>
235
+ <span {...stylex.props(popup.itemMeta)}>{STATUS_LABEL[doc.status]}</span>
236
+ </div>
237
+ </li>
238
+ );
239
+ })}
240
240
  </ul>
241
241
  ) : null}
242
242
  {changes.length > 0 ? <div {...stylex.props(popup.separator)} /> : null}
@@ -311,6 +311,9 @@ const styles = stylex.create({
311
311
  textOverflow: "ellipsis",
312
312
  whiteSpace: "nowrap",
313
313
  },
314
+ invalidName: {
315
+ color: colors.danger,
316
+ },
314
317
  footer: {
315
318
  display: "flex",
316
319
  flexDirection: "column",