@backmatter/studio 0.0.4 → 0.0.6

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.
@@ -1,18 +1,19 @@
1
1
  // packages/studio/src/site-widget.tsx
2
- // The on-site bar: agent and publish in one tree. The host page mounts this
2
+ // The on-site bar: Live/Preview, the current page, the branch, then Agent and Publish. The host page mounts this
3
3
  // after the site session verifies. Calls go through the host's same-origin
4
4
  // CMS proxy.
5
- import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
5
+ import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
6
6
  import * as stylex from "@stylexjs/stylex";
7
7
  import { ChatPanel } from "./chat/ChatPanel";
8
- import { PublishIcon, SparkIcon } from "./chat/icons";
9
8
  import { chatPointerFromGrab } from "./chat/chat-context";
10
9
  import { POINTER_ARM_EVENT } from "./chat/Composer";
11
10
  import { ChatProvider, ChatSessionScope, useChatUI } from "./chat/ChatProvider";
12
11
  import { isPanelVisible } from "./chat/chat-state";
13
12
  import { DOC_CHANGED_EVENT, EditorFocusProvider, useEditorFocus } from "./chrome/EditorFocusContext";
14
- import { matchAppHotkey, modifierSymbol } from "./chrome/hotkeys";
13
+ import { matchAppHotkey } from "./chrome/hotkeys";
15
14
  import { EmbedPublishPage } from "./pages/EmbedPublishPage";
15
+ import { PreviewFrame } from "./preview-frame";
16
+ import { DEVBAR_CLASS, SiteBar } from "./site-bar";
16
17
  import { installSitePointer } from "./site-pointer";
17
18
  import { lightTheme } from "./styles/themes";
18
19
  import { configureStudio } from "./studio-config";
@@ -30,28 +31,36 @@ export type SiteWidgetProps = {
30
31
  doc: SiteWidgetDoc | null;
31
32
  branch?: string;
32
33
  tree?: string;
34
+ siteMode?: "preview" | "live";
33
35
  exiting?: boolean;
34
36
  onExit: () => void;
37
+ onEnterPreview?: () => void;
35
38
  /** The host re-renders the page after a write. */
36
39
  onDocChanged?: () => void;
37
40
  /** The site session was rejected. The host drops the widget. */
38
41
  onSessionExpired?: () => void;
39
42
  /** Same-origin CMS proxy. Default `/api/backmatter/cms`. */
40
43
  proxyPath?: string;
44
+ /** Host route that turns a preview ticket into the site session. */
45
+ previewEntryPath?: string;
46
+ /** Full-viewport preview ring and top pill (see `devBar.previewFrame` in host config). */
47
+ previewFrame?: boolean;
41
48
  version?: string;
42
49
  };
43
50
 
44
- const DEVBAR_CLASS = "backmatter-devbar";
45
-
46
51
  export function SiteWidget({
47
52
  doc,
48
53
  branch,
49
54
  tree,
55
+ siteMode = "preview",
50
56
  exiting,
51
57
  onExit,
58
+ onEnterPreview,
52
59
  onDocChanged,
53
60
  onSessionExpired,
54
61
  proxyPath = "/api/backmatter/cms",
62
+ previewEntryPath,
63
+ previewFrame,
55
64
  version,
56
65
  }: SiteWidgetProps) {
57
66
  const expiredRef = useRef(onSessionExpired);
@@ -94,50 +103,27 @@ export function SiteWidget({
94
103
  }, []);
95
104
 
96
105
  const bar = (
97
- <footer aria-label="Backmatter" className={DEVBAR_CLASS} style={barStyle}>
98
- <span style={docLabel}>{label}</span>
99
- <BarText>Preview</BarText>
100
- {branch ? <BarText>{branch}</BarText> : null}
101
- {tree ? <BarText>draft</BarText> : null}
102
- <button
103
- type="button"
104
- className={`${DEVBAR_CLASS}-btn${publishOpen ? ` ${DEVBAR_CLASS}-on` : ""}`}
105
- aria-pressed={publishOpen}
106
- disabled={!doc}
107
- onClick={() => {
108
- setChatOpen(false);
109
- setPublishOpen((open) => !open);
110
- }}
111
- >
112
- <PublishIcon />
113
- <span>Publish{changeCount ? ` ${changeCount}` : ""}</span>
114
- </button>
115
- <button
116
- type="button"
117
- className={`${DEVBAR_CLASS}-btn ${DEVBAR_CLASS}-agent${chatOpen ? ` ${DEVBAR_CLASS}-on` : ""}`}
118
- aria-pressed={chatOpen}
119
- aria-keyshortcuts="Meta+J Control+J"
120
- disabled={!doc}
121
- onClick={() => {
122
- setPublishOpen(false);
123
- setChatOpen((open) => !open);
124
- }}
125
- >
126
- <SparkIcon />
127
- <span>Agent</span>
128
- <kbd>
129
- {modifierSymbol()}J
130
- </kbd>
131
- </button>
132
- <button type="button" aria-label="Exit preview" disabled={exiting} onClick={onExit} style={barButton}>
133
- Exit
134
- </button>
135
- </footer>
106
+ <SiteBar
107
+ label={label}
108
+ branch={branch}
109
+ tree={tree}
110
+ doc={doc}
111
+ siteMode={siteMode}
112
+ exiting={exiting}
113
+ chatOpen={chatOpen}
114
+ publishOpen={publishOpen}
115
+ changeCount={changeCount}
116
+ previewEntryPath={previewEntryPath}
117
+ onExit={onExit}
118
+ onEnterPreview={onEnterPreview}
119
+ onToggleChat={() => setChatOpen((open) => !open)}
120
+ onTogglePublish={() => setPublishOpen((open) => !open)}
121
+ />
136
122
  );
137
123
 
138
124
  return (
139
125
  <>
140
- <style>{BAR_CSS}</style>
126
+ {previewFrame ? <PreviewFrame /> : null}
141
127
  {doc ? (
142
128
  <TooltipProvider>
143
129
  <EditorFocusProvider>
@@ -240,7 +226,11 @@ function SitePointer({ doc }: { doc: SiteWidgetDoc }) {
240
226
  },
241
227
  onGrab: (grab) => {
242
228
  const next = chatPointerFromGrab(docRef.current, grab);
243
- if (next) addPointer(next);
229
+ if (!next) return;
230
+ addPointer(next);
231
+ requestAnimationFrame(() => {
232
+ window.dispatchEvent(new Event("backmatter:chat-focus"));
233
+ });
244
234
  },
245
235
  onEscape: () => {
246
236
  window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on: false } }));
@@ -261,69 +251,10 @@ function SitePointer({ doc }: { doc: SiteWidgetDoc }) {
261
251
  return null;
262
252
  }
263
253
 
264
- function BarText({ children }: { children: ReactNode }) {
265
- return <span style={mutedStyle}>{children}</span>;
266
- }
267
-
268
- const barStyle: CSSProperties = {
269
- position: "fixed",
270
- right: "max(16px, env(safe-area-inset-right, 0px))",
271
- bottom: 0,
272
- zIndex: 2147483000,
273
- isolation: "isolate",
274
- display: "flex",
275
- alignItems: "center",
276
- gap: 2,
277
- width: "max-content",
278
- maxWidth: "calc(100vw - 32px)",
279
- minHeight: 36,
280
- padding: "0 6px 0 10px",
281
- boxSizing: "border-box",
282
- border: "1px solid #e4e4e7",
283
- borderRadius: "12px 12px 0 0",
284
- background: "#ffffff",
285
- color: "#71717a",
286
- boxShadow: "0 10px 28px rgba(24, 24, 27, 0.12), 0 1px 2px rgba(24, 24, 27, 0.06)",
287
- font: '12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
288
- WebkitFontSmoothing: "antialiased",
289
- };
290
-
291
- const barButton: CSSProperties = {
292
- border: 0,
293
- borderRadius: 8,
294
- background: "transparent",
295
- color: "#18181b",
296
- font: "inherit",
297
- cursor: "pointer",
298
- padding: "0 8px",
299
- minHeight: 28,
300
- };
301
-
302
- const docLabel: CSSProperties = {
303
- fontWeight: 600,
304
- color: "#18181b",
305
- maxWidth: 200,
306
- overflow: "hidden",
307
- textOverflow: "ellipsis",
308
- whiteSpace: "nowrap",
309
- padding: "0 6px",
310
- };
311
-
312
- const mutedStyle: CSSProperties = {
313
- color: "#71717a",
314
- fontSize: 12,
315
- margin: 0,
316
- padding: "0 6px",
317
- maxWidth: 140,
318
- overflow: "hidden",
319
- textOverflow: "ellipsis",
320
- whiteSpace: "nowrap",
321
- };
322
-
323
254
  const publishStyle: CSSProperties = {
324
255
  position: "fixed",
325
256
  right: "max(16px, env(safe-area-inset-right, 0px))",
326
- bottom: "calc(env(safe-area-inset-bottom, 0px) + 48px)",
257
+ bottom: "calc(48px + env(safe-area-inset-bottom, 0px))",
327
258
  zIndex: 2147483001,
328
259
  width: "min(320px, calc(100vw - 32px))",
329
260
  maxHeight: "min(420px, calc(100vh - 80px))",
@@ -333,59 +264,3 @@ const publishStyle: CSSProperties = {
333
264
  background: "#ffffff",
334
265
  boxShadow: "0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06)",
335
266
  };
336
-
337
- const BAR_CSS = `
338
- @keyframes backmatter-devbar-in {
339
- from { opacity: 0; transform: translateY(10px) scale(0.98); }
340
- to { opacity: 1; transform: none; }
341
- }
342
- .${DEVBAR_CLASS} { animation: backmatter-devbar-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both; }
343
- @media (prefers-reduced-motion: reduce) {
344
- .${DEVBAR_CLASS} { animation: none; }
345
- }
346
- .${DEVBAR_CLASS}-btn {
347
- display: inline-flex;
348
- align-items: center;
349
- gap: 6px;
350
- height: 26px;
351
- margin-inline: 4px;
352
- padding: 0 8px;
353
- border: 1px solid #e4e4e7;
354
- border-radius: 6px;
355
- background: #ffffff;
356
- color: #18181b;
357
- font-size: 11px;
358
- font-weight: 500;
359
- line-height: 1;
360
- cursor: pointer;
361
- }
362
- .${DEVBAR_CLASS}-agent kbd {
363
- font-family: Geist Mono, ui-monospace, SFMono-Regular, Menlo, monospace;
364
- font-size: 11px;
365
- font-weight: 400;
366
- opacity: 0.7;
367
- }
368
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on,
369
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on:hover:not(:disabled) {
370
- background: #18181b;
371
- border-color: #18181b;
372
- color: #fafafa;
373
- }
374
- @media (hover: hover) {
375
- .${DEVBAR_CLASS} button:hover:not(:disabled) { background: #f4f4f5; }
376
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-btn:hover:not(:disabled) {
377
- background: rgba(24, 24, 27, 0.05);
378
- }
379
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on:hover:not(:disabled) {
380
- background: #18181b;
381
- }
382
- }
383
- .${DEVBAR_CLASS} button:focus-visible { outline: 2px solid #18181b; outline-offset: 2px; }
384
- .${DEVBAR_CLASS} button:disabled { cursor: default; opacity: 0.5; }
385
- html.backmatter-pointing, html.backmatter-pointing * { cursor: crosshair !important; }
386
- html.backmatter-pointing .${DEVBAR_CLASS},
387
- html.backmatter-pointing .${DEVBAR_CLASS} *,
388
- html.backmatter-pointing [data-backmatter-panel],
389
- html.backmatter-pointing [data-backmatter-panel] * { cursor: pointer !important; }
390
- html.backmatter-pointing .${DEVBAR_CLASS} button:disabled { cursor: default !important; }
391
- `;