@backmatter/studio 0.0.5 → 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>
@@ -265,69 +251,10 @@ function SitePointer({ doc }: { doc: SiteWidgetDoc }) {
265
251
  return null;
266
252
  }
267
253
 
268
- function BarText({ children }: { children: ReactNode }) {
269
- return <span style={mutedStyle}>{children}</span>;
270
- }
271
-
272
- const barStyle: CSSProperties = {
273
- position: "fixed",
274
- right: "max(16px, env(safe-area-inset-right, 0px))",
275
- bottom: 0,
276
- zIndex: 2147483000,
277
- isolation: "isolate",
278
- display: "flex",
279
- alignItems: "center",
280
- gap: 2,
281
- width: "max-content",
282
- maxWidth: "calc(100vw - 32px)",
283
- minHeight: 36,
284
- padding: "0 6px 0 10px",
285
- boxSizing: "border-box",
286
- border: "1px solid #e4e4e7",
287
- borderRadius: "12px 12px 0 0",
288
- background: "#ffffff",
289
- color: "#71717a",
290
- boxShadow: "0 10px 28px rgba(24, 24, 27, 0.12), 0 1px 2px rgba(24, 24, 27, 0.06)",
291
- font: '12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
292
- WebkitFontSmoothing: "antialiased",
293
- };
294
-
295
- const barButton: CSSProperties = {
296
- border: 0,
297
- borderRadius: 8,
298
- background: "transparent",
299
- color: "#18181b",
300
- font: "inherit",
301
- cursor: "pointer",
302
- padding: "0 8px",
303
- minHeight: 28,
304
- };
305
-
306
- const docLabel: CSSProperties = {
307
- fontWeight: 600,
308
- color: "#18181b",
309
- maxWidth: 200,
310
- overflow: "hidden",
311
- textOverflow: "ellipsis",
312
- whiteSpace: "nowrap",
313
- padding: "0 6px",
314
- };
315
-
316
- const mutedStyle: CSSProperties = {
317
- color: "#71717a",
318
- fontSize: 12,
319
- margin: 0,
320
- padding: "0 6px",
321
- maxWidth: 140,
322
- overflow: "hidden",
323
- textOverflow: "ellipsis",
324
- whiteSpace: "nowrap",
325
- };
326
-
327
254
  const publishStyle: CSSProperties = {
328
255
  position: "fixed",
329
256
  right: "max(16px, env(safe-area-inset-right, 0px))",
330
- bottom: "calc(env(safe-area-inset-bottom, 0px) + 48px)",
257
+ bottom: "calc(48px + env(safe-area-inset-bottom, 0px))",
331
258
  zIndex: 2147483001,
332
259
  width: "min(320px, calc(100vw - 32px))",
333
260
  maxHeight: "min(420px, calc(100vh - 80px))",
@@ -337,59 +264,3 @@ const publishStyle: CSSProperties = {
337
264
  background: "#ffffff",
338
265
  boxShadow: "0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06)",
339
266
  };
340
-
341
- const BAR_CSS = `
342
- @keyframes backmatter-devbar-in {
343
- from { opacity: 0; transform: translateY(10px) scale(0.98); }
344
- to { opacity: 1; transform: none; }
345
- }
346
- .${DEVBAR_CLASS} { animation: backmatter-devbar-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both; }
347
- @media (prefers-reduced-motion: reduce) {
348
- .${DEVBAR_CLASS} { animation: none; }
349
- }
350
- .${DEVBAR_CLASS}-btn {
351
- display: inline-flex;
352
- align-items: center;
353
- gap: 6px;
354
- height: 26px;
355
- margin-inline: 4px;
356
- padding: 0 8px;
357
- border: 1px solid #e4e4e7;
358
- border-radius: 6px;
359
- background: #ffffff;
360
- color: #18181b;
361
- font-size: 11px;
362
- font-weight: 500;
363
- line-height: 1;
364
- cursor: pointer;
365
- }
366
- .${DEVBAR_CLASS}-agent kbd {
367
- font-family: Geist Mono, ui-monospace, SFMono-Regular, Menlo, monospace;
368
- font-size: 11px;
369
- font-weight: 400;
370
- opacity: 0.7;
371
- }
372
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on,
373
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on:hover:not(:disabled) {
374
- background: #18181b;
375
- border-color: #18181b;
376
- color: #fafafa;
377
- }
378
- @media (hover: hover) {
379
- .${DEVBAR_CLASS} button:hover:not(:disabled) { background: #f4f4f5; }
380
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-btn:hover:not(:disabled) {
381
- background: rgba(24, 24, 27, 0.05);
382
- }
383
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on:hover:not(:disabled) {
384
- background: #18181b;
385
- }
386
- }
387
- .${DEVBAR_CLASS} button:focus-visible { outline: 2px solid #18181b; outline-offset: 2px; }
388
- .${DEVBAR_CLASS} button:disabled { cursor: default; opacity: 0.5; }
389
- html.backmatter-pointing, html.backmatter-pointing * { cursor: crosshair !important; }
390
- html.backmatter-pointing .${DEVBAR_CLASS},
391
- html.backmatter-pointing .${DEVBAR_CLASS} *,
392
- html.backmatter-pointing [data-backmatter-panel],
393
- html.backmatter-pointing [data-backmatter-panel] * { cursor: pointer !important; }
394
- html.backmatter-pointing .${DEVBAR_CLASS} button:disabled { cursor: default !important; }
395
- `;