@backmatter/studio 0.0.8 → 0.0.10

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 (65) hide show
  1. package/dist/chunks/UiKitApp-BxvTwsko.js +2410 -0
  2. package/dist/chunks/WorkspaceRoutes-BKKEkAq7.js +90126 -0
  3. package/dist/chunks/{dashboard-bNp6AbKE.js → dashboard-8rjkfEXl.js} +380 -382
  4. package/dist/chunks/{index-DoDE-hwB.js → index-laWAz5_p.js} +111 -112
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +287 -146
  7. package/dist/site-widget.css +1615 -0
  8. package/dist/site-widget.js +30903 -628
  9. package/dist/studio.css +268 -67
  10. package/package.json +4 -4
  11. package/src/App.tsx +12 -29
  12. package/src/api.ts +55 -0
  13. package/src/chat/ChangeCard.tsx +2 -0
  14. package/src/chat/ChatMessage.tsx +73 -62
  15. package/src/chat/ChatPanel.tsx +110 -15
  16. package/src/chat/DocListCard.tsx +2 -0
  17. package/src/chat/LiveStatus.tsx +78 -8
  18. package/src/chat/Reveal.tsx +34 -0
  19. package/src/chat/Shimmer.tsx +65 -23
  20. package/src/chat/chat-helpers.ts +35 -0
  21. package/src/chat/motion.ts +73 -0
  22. package/src/chat/useStickToBottom.ts +129 -14
  23. package/src/chrome/ContentExplorerSidebar.tsx +23 -8
  24. package/src/chrome/content-tree-visibility.ts +55 -0
  25. package/src/devbar-state.ts +125 -0
  26. package/src/index.css +3 -131
  27. package/src/pages/CliAuthorizePage.tsx +61 -28
  28. package/src/pages/MediaGalleryPage.tsx +74 -11
  29. package/src/pages/MediaLightbox.tsx +93 -1
  30. package/src/pages/SettingsUsagePage.tsx +23 -1
  31. package/src/pages/media-grid.ts +13 -0
  32. package/src/pages/ui-kit/UiKitApp.tsx +164 -0
  33. package/src/pages/ui-kit/buttons.tsx +30 -0
  34. package/src/pages/ui-kit/catalog.tsx +41 -0
  35. package/src/pages/ui-kit/colors.tsx +95 -0
  36. package/src/pages/ui-kit/devbar.tsx +160 -0
  37. package/src/pages/ui-kit/feedback.tsx +39 -0
  38. package/src/pages/ui-kit/fields.tsx +47 -0
  39. package/src/pages/ui-kit/lists.tsx +19 -0
  40. package/src/pages/ui-kit/overlays.tsx +39 -0
  41. package/src/pages/ui-kit/path.ts +9 -0
  42. package/src/pages/ui-kit/sections.tsx +32 -0
  43. package/src/pages/ui-kit/surfaces.tsx +121 -0
  44. package/src/pages/ui-kit/topic.tsx +112 -0
  45. package/src/pages/ui-kit/topics.ts +71 -0
  46. package/src/pages/ui-kit/type.tsx +55 -0
  47. package/src/preview-frame-radius.ts +51 -0
  48. package/src/preview-frame.tsx +108 -49
  49. package/src/shell-resets.css +155 -0
  50. package/src/site-bar.tsx +1810 -425
  51. package/src/site-dashboard-href.ts +21 -0
  52. package/src/site-devbar-overlay-inject.ts +15 -0
  53. package/src/site-devbar-overlay.css +217 -0
  54. package/src/site-pointer.ts +1 -1
  55. package/src/site-publish-menu.tsx +33 -0
  56. package/src/site-widget-styles.ts +7 -0
  57. package/src/site-widget.tsx +61 -21
  58. package/src/studio-prose-ui.css +39 -0
  59. package/src/theme/ThemeProvider.tsx +4 -1
  60. package/src/workspace-path.ts +10 -6
  61. package/dist/chunks/EmbedPublishPage-CtIlaeGO.js +0 -149
  62. package/dist/chunks/UiKitPage-DcnSQ-Hf.js +0 -173
  63. package/dist/chunks/WorkspaceRoutes-DVpUOaMV.js +0 -59550
  64. package/dist/chunks/status-bar-action-C0fsAWvD.js +0 -30618
  65. package/src/pages/UiKitPage.tsx +0 -331
@@ -0,0 +1,21 @@
1
+ // packages/studio/src/site-dashboard-href.ts
2
+ import { contentDocPath } from "./workspace-path";
3
+
4
+ export type DashboardDocRef = {
5
+ locale: string;
6
+ type: string;
7
+ slug: string;
8
+ };
9
+
10
+ /** Dashboard URL for the current site document, or the dashboard origin when unknown. */
11
+ export function resolveDashboardOpenHref(
12
+ dashboardOrigin: string | undefined,
13
+ doc: DashboardDocRef | null,
14
+ workspaceSlug: string | null,
15
+ defaultLocale = "en",
16
+ ): string | undefined {
17
+ if (!dashboardOrigin?.trim()) return undefined;
18
+ const base = dashboardOrigin.replace(/\/+$/, "");
19
+ if (!doc || !workspaceSlug) return base;
20
+ return `${base}${contentDocPath(workspaceSlug, doc.type, doc.slug, doc.locale, defaultLocale)}`;
21
+ }
@@ -0,0 +1,15 @@
1
+ // packages/studio/src/site-devbar-overlay-inject.ts
2
+ // Injected with the widget JS so page/publish menus stay styled when the host
3
+ // loads only `site-widget.js` or the CSS chunk is stale.
4
+ import overlayCss from "./site-devbar-overlay.css?inline";
5
+
6
+ const STYLE_ID = "backmatter-devbar-overlay-styles";
7
+
8
+ export function ensureDevbarOverlayStyles(): void {
9
+ if (typeof document === "undefined") return;
10
+ if (document.getElementById(STYLE_ID)) return;
11
+ const style = document.createElement("style");
12
+ style.id = STYLE_ID;
13
+ style.textContent = overlayCss;
14
+ document.head.appendChild(style);
15
+ }
@@ -0,0 +1,217 @@
1
+ /* packages/studio/src/site-devbar-overlay.css
2
+ Floating devbar menus (page, publish, settings) in the light DOM. */
3
+
4
+ .backmatter-devbar-overlay {
5
+ --panel: #ffffff;
6
+ --line: #e7e7e4;
7
+ --line2: #d8d8d4;
8
+ --hover: #f3f3f1;
9
+ --fg: #17171a;
10
+ --fg2: #5d5d62;
11
+ --fg3: #8e8e93;
12
+ --shadow-live: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 28px -8px rgba(0, 0, 0, 0.18);
13
+ position: fixed;
14
+ z-index: 2147483002;
15
+ display: flex;
16
+ flex-direction: column;
17
+ box-sizing: border-box;
18
+ border: 1px solid var(--line);
19
+ border-radius: 12px;
20
+ background: var(--panel);
21
+ color: var(--fg);
22
+ box-shadow: var(--shadow-live);
23
+ font-family: "Geist Sans", Geist, ui-sans-serif, system-ui, sans-serif;
24
+ font-size: 12.5px;
25
+ font-weight: 500;
26
+ line-height: 1.3;
27
+ -webkit-font-smoothing: antialiased;
28
+ }
29
+
30
+ .backmatter-devbar-overlay.backmatter-devbar-menu {
31
+ width: 240px;
32
+ max-height: min(320px, calc(100dvh - 96px));
33
+ overflow: auto;
34
+ padding: 6px;
35
+ }
36
+
37
+ .backmatter-devbar-overlay.backmatter-devbar-menu-pages {
38
+ width: min(280px, calc(100vw - 24px));
39
+ }
40
+
41
+ .backmatter-devbar-overlay.backmatter-devbar-publish-panel {
42
+ width: min(320px, calc(100vw - 24px));
43
+ padding: 0;
44
+ overflow: hidden;
45
+ max-height: min(420px, calc(100dvh - 96px));
46
+ }
47
+
48
+ .backmatter-devbar-overlay.backmatter-devbar-settings {
49
+ width: min(328px, calc(100vw - 24px));
50
+ max-height: min(480px, calc(100dvh - 96px));
51
+ padding: 8px;
52
+ border-radius: 16px;
53
+ overflow: auto;
54
+ }
55
+
56
+ .backmatter-devbar-overlay input {
57
+ width: 100%;
58
+ height: 32px;
59
+ margin: 2px 0 6px;
60
+ padding: 0 10px;
61
+ box-sizing: border-box;
62
+ border: 1px solid var(--line);
63
+ border-radius: 7px;
64
+ background: #fff;
65
+ color: var(--fg);
66
+ font: inherit;
67
+ font-size: 16px;
68
+ cursor: text;
69
+ user-select: text;
70
+ }
71
+
72
+ .backmatter-devbar-overlay button {
73
+ margin: 0;
74
+ padding: 0;
75
+ border: 0;
76
+ background: transparent;
77
+ color: inherit;
78
+ font: inherit;
79
+ text-align: left;
80
+ cursor: pointer;
81
+ -webkit-appearance: none;
82
+ appearance: none;
83
+ }
84
+
85
+ .backmatter-devbar-overlay .backmatter-devbar-option {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 8px;
89
+ width: 100%;
90
+ min-height: 32px;
91
+ padding: 0 10px;
92
+ border-radius: 8px;
93
+ box-sizing: border-box;
94
+ }
95
+
96
+ .backmatter-devbar-overlay .backmatter-devbar-option > span:first-child {
97
+ min-width: 0;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ .backmatter-devbar-overlay .backmatter-devbar-current {
104
+ background: var(--hover);
105
+ }
106
+
107
+ .backmatter-devbar-overlay .backmatter-devbar-meta {
108
+ margin-left: auto;
109
+ flex-shrink: 0;
110
+ color: var(--fg3);
111
+ font-size: 11px;
112
+ font-weight: 500;
113
+ }
114
+
115
+ .backmatter-devbar-overlay .backmatter-devbar-note {
116
+ margin: 0;
117
+ padding: 8px;
118
+ color: var(--fg2);
119
+ font-size: 12px;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .backmatter-devbar-overlay .backmatter-devbar-note-error {
124
+ color: #b91c1c;
125
+ }
126
+
127
+ .backmatter-devbar-overlay .backmatter-devbar-menu-split {
128
+ margin-top: 4px;
129
+ padding-top: 4px;
130
+ border-top: 1px solid var(--line);
131
+ }
132
+
133
+ .backmatter-devbar-overlay .backmatter-devbar-settings-head {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ gap: 12px;
138
+ min-height: 28px;
139
+ padding: 4px 12px 8px;
140
+ }
141
+
142
+ .backmatter-devbar-overlay .backmatter-devbar-settings-kicker {
143
+ color: var(--fg3);
144
+ font-size: 11px;
145
+ font-weight: 600;
146
+ letter-spacing: 0.08em;
147
+ text-transform: uppercase;
148
+ }
149
+
150
+ .backmatter-devbar-overlay .backmatter-devbar-settings-place {
151
+ min-width: 0;
152
+ overflow: hidden;
153
+ color: var(--fg3);
154
+ font-size: 12px;
155
+ font-weight: 500;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .backmatter-devbar-overlay .backmatter-devbar-settings-row,
161
+ .backmatter-devbar-overlay .backmatter-devbar-settings-action,
162
+ .backmatter-devbar-overlay button.backmatter-devbar-settings-action {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 10px;
166
+ width: 100%;
167
+ min-height: 40px;
168
+ padding: 0 12px;
169
+ border-radius: 8px;
170
+ box-sizing: border-box;
171
+ }
172
+
173
+ .backmatter-devbar-overlay .backmatter-devbar-settings-rule {
174
+ height: 1px;
175
+ margin: 6px 12px;
176
+ background: var(--line);
177
+ }
178
+
179
+ .backmatter-devbar-overlay .backmatter-devbar-segment {
180
+ display: inline-flex;
181
+ flex-shrink: 0;
182
+ padding: 2px;
183
+ border-radius: 9px;
184
+ background: var(--hover);
185
+ }
186
+
187
+ .backmatter-devbar-overlay button.backmatter-devbar-segment-btn {
188
+ height: 26px;
189
+ padding: 0 10px;
190
+ border-radius: 7px;
191
+ color: var(--fg2);
192
+ font-size: 12.5px;
193
+ }
194
+
195
+ .backmatter-devbar-overlay button.backmatter-devbar-segment-on {
196
+ background: #fff;
197
+ color: var(--fg);
198
+ font-weight: 600;
199
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
200
+ }
201
+
202
+ .backmatter-devbar-overlay .backmatter-devbar-kbd {
203
+ margin-left: auto;
204
+ color: var(--fg3);
205
+ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
206
+ font-size: 11px;
207
+ font-weight: 500;
208
+ }
209
+
210
+ @media (hover: hover) {
211
+ .backmatter-devbar-overlay .backmatter-devbar-option:hover:not(:disabled) {
212
+ background: var(--hover);
213
+ }
214
+ .backmatter-devbar-overlay button.backmatter-devbar-settings-action:hover {
215
+ background: var(--hover);
216
+ }
217
+ }
@@ -205,7 +205,7 @@ function positionPointerOutline(outline: HTMLDivElement, target: Element) {
205
205
  }
206
206
 
207
207
  function isSiteChrome(el: Element): boolean {
208
- return el.closest("[data-backmatter-panel], .backmatter-devbar, [data-backmatter-pointer]") !== null;
208
+ return el.closest("[data-backmatter-panel], .backmatter-devbar, [data-backmatter-devbar], [data-backmatter-pointer]") !== null;
209
209
  }
210
210
 
211
211
  /** The page element a stored pointer refers to. Chat chrome is ignored. */
@@ -0,0 +1,33 @@
1
+ // packages/studio/src/site-publish-menu.tsx
2
+ // Publish popover for the on-site bar (StyleX). Loaded on demand so site-bar tests stay StyleX-free.
3
+ import { createPortal } from "react-dom";
4
+ import type { Ref } from "react";
5
+ import { EmbedPublishPage } from "./pages/EmbedPublishPage";
6
+ import { DEVBAR_CLASS, DEVBAR_OVERLAY_CLASS, devbarOverlayRoot } from "./site-bar";
7
+
8
+ export function PublishMenu({
9
+ menuRef,
10
+ workspaceId,
11
+ onStatus,
12
+ onPublished,
13
+ }: {
14
+ menuRef: Ref<HTMLDivElement | null>;
15
+ workspaceId: string;
16
+ onStatus?: (status: { changeCount: number; conflictCount: number }) => void;
17
+ onPublished?: () => void;
18
+ }) {
19
+ const root = devbarOverlayRoot();
20
+ if (!root) return null;
21
+ return createPortal(
22
+ <div
23
+ ref={menuRef}
24
+ role="dialog"
25
+ aria-label="Publish"
26
+ className={`backmatter-studio ${DEVBAR_OVERLAY_CLASS} ${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-publish-panel`}
27
+ data-backmatter-panel=""
28
+ >
29
+ <EmbedPublishPage workspaceId={workspaceId} onStatus={onStatus} onPublished={onPublished} />
30
+ </div>,
31
+ root,
32
+ );
33
+ }
@@ -0,0 +1,7 @@
1
+ // packages/studio/src/site-widget-styles.ts
2
+ // On-site widget stylesheet entry: scoped resets only. Full studio chrome
3
+ // (fonts, prose-ui, katex) lives in styles.ts → dist/studio.css.
4
+ import "@fontsource/geist-sans/500.css";
5
+ import "@fontsource/geist-mono/500.css";
6
+ import "./shell-resets.css";
7
+ import "./site-devbar-overlay.css";
@@ -1,8 +1,9 @@
1
1
  // packages/studio/src/site-widget.tsx
2
- // The on-site bar: Live/Preview, the current page, the branch, then Agent and Publish. The host page mounts this
2
+ import "./site-widget-styles";
3
+ // The on-site bar: Enter/Exit preview, the current page, then Agent and Publish. The host page mounts this
3
4
  // after the site session verifies. Calls go through the host's same-origin
4
5
  // CMS proxy.
5
- import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
6
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
6
7
  import * as stylex from "@stylexjs/stylex";
7
8
  import { ChatPanel } from "./chat/ChatPanel";
8
9
  import { chatPointerFromGrab } from "./chat/chat-context";
@@ -12,8 +13,7 @@ import { isPanelVisible } from "./chat/chat-state";
12
13
  import { DOC_CHANGED_EVENT, EditorFocusProvider, useEditorFocus } from "./chrome/EditorFocusContext";
13
14
  import { matchAppHotkey } from "./chrome/hotkeys";
14
15
  import { EmbedPublishPage } from "./pages/EmbedPublishPage";
15
- import { PreviewFrame } from "./preview-frame";
16
- import { DEVBAR_CLASS, SiteBar } from "./site-bar";
16
+ import { SiteBar } from "./site-bar";
17
17
  import { installSitePointer } from "./site-pointer";
18
18
  import { lightTheme } from "./styles/themes";
19
19
  import { configureStudio } from "./studio-config";
@@ -45,7 +45,11 @@ export type SiteWidgetProps = {
45
45
  previewEntryPath?: string;
46
46
  /** Full-viewport preview ring and top pill (see `devBar.previewFrame` in host config). */
47
47
  previewFrame?: boolean;
48
+ previewFrameRadius?: number;
48
49
  version?: string;
50
+ /** Studio origin. The settings menu opens this in the current tab. */
51
+ dashboardHref?: string;
52
+ onSignOut?: () => void;
49
53
  };
50
54
 
51
55
  export function SiteWidget({
@@ -61,7 +65,10 @@ export function SiteWidget({
61
65
  proxyPath = "/api/backmatter/cms",
62
66
  previewEntryPath,
63
67
  previewFrame,
68
+ previewFrameRadius,
64
69
  version,
70
+ dashboardHref,
71
+ onSignOut,
65
72
  }: SiteWidgetProps) {
66
73
  const expiredRef = useRef(onSessionExpired);
67
74
  expiredRef.current = onSessionExpired;
@@ -85,7 +92,11 @@ export function SiteWidget({
85
92
  const [chatOpen, setChatOpen] = useState(false);
86
93
  const [publishOpen, setPublishOpen] = useState(false);
87
94
  const [changeCount, setChangeCount] = useState(0);
95
+ const [publishAnchor, setPublishAnchor] = useState<DOMRect | null>(null);
88
96
  const label = doc ? `${doc.type}/${doc.slug}` : "No document";
97
+ const onPublishAnchor = useCallback((rect: DOMRect | null) => {
98
+ setPublishAnchor(rect);
99
+ }, []);
89
100
 
90
101
  function wrote() {
91
102
  onDocChanged?.();
@@ -102,6 +113,23 @@ export function SiteWidget({
102
113
  return () => document.removeEventListener("keydown", onKey, true);
103
114
  }, []);
104
115
 
116
+ useEffect(() => {
117
+ if (!publishOpen) return;
118
+ function onPointer(event: PointerEvent) {
119
+ for (const node of event.composedPath()) {
120
+ if (node instanceof HTMLElement) {
121
+ if (node.matches("[data-backmatter-panel], [data-backmatter-devbar]")) return;
122
+ }
123
+ if (node instanceof ShadowRoot && node.host instanceof HTMLElement) {
124
+ if (node.host.hasAttribute("data-backmatter-devbar")) return;
125
+ }
126
+ }
127
+ setPublishOpen(false);
128
+ }
129
+ document.addEventListener("pointerdown", onPointer);
130
+ return () => document.removeEventListener("pointerdown", onPointer);
131
+ }, [publishOpen]);
132
+
105
133
  const bar = (
106
134
  <SiteBar
107
135
  label={label}
@@ -116,14 +144,18 @@ export function SiteWidget({
116
144
  previewEntryPath={previewEntryPath}
117
145
  onExit={onExit}
118
146
  onEnterPreview={onEnterPreview}
147
+ frame={Boolean(previewFrame)}
148
+ frameRadius={previewFrameRadius}
119
149
  onToggleChat={() => setChatOpen((open) => !open)}
120
150
  onTogglePublish={() => setPublishOpen((open) => !open)}
151
+ dashboardHref={dashboardHref}
152
+ onSignOut={onSignOut}
153
+ onPublishAnchor={onPublishAnchor}
121
154
  />
122
155
  );
123
156
 
124
157
  return (
125
158
  <>
126
- {previewFrame ? <PreviewFrame /> : null}
127
159
  {doc ? (
128
160
  <TooltipProvider>
129
161
  <EditorFocusProvider>
@@ -138,8 +170,8 @@ export function SiteWidget({
138
170
  </EditorFocusProvider>
139
171
  </TooltipProvider>
140
172
  ) : null}
141
- {doc ? (
142
- <div data-backmatter-panel="" hidden={!publishOpen} style={publishStyle}>
173
+ {doc && publishOpen ? (
174
+ <div data-backmatter-panel="" style={publishPanelStyle(publishAnchor)}>
143
175
  <EmbedPublishPage
144
176
  workspaceId={doc.workspaceId}
145
177
  onStatus={({ changeCount: count }) => setChangeCount(count)}
@@ -218,7 +250,7 @@ function SitePointer({ doc }: { doc: SiteWidgetDoc }) {
218
250
 
219
251
  useEffect(() => {
220
252
  const pointer = installSitePointer(document, {
221
- bar: () => document.querySelector(`.${DEVBAR_CLASS}`),
253
+ bar: () => document.querySelector("[data-backmatter-devbar]"),
222
254
  ignore: (target) => {
223
255
  if (!(target instanceof Element)) return true;
224
256
  if (target.closest("[data-backmatter-panel]")) return true;
@@ -251,16 +283,24 @@ function SitePointer({ doc }: { doc: SiteWidgetDoc }) {
251
283
  return null;
252
284
  }
253
285
 
254
- const publishStyle: CSSProperties = {
255
- position: "fixed",
256
- right: "max(16px, env(safe-area-inset-right, 0px))",
257
- bottom: "calc(48px + env(safe-area-inset-bottom, 0px))",
258
- zIndex: 2147483001,
259
- width: "min(320px, calc(100vw - 32px))",
260
- maxHeight: "min(420px, calc(100vh - 80px))",
261
- overflow: "auto",
262
- border: "1px solid #e4e4e7",
263
- borderRadius: 10,
264
- background: "#ffffff",
265
- boxShadow: "0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06)",
266
- };
286
+ function publishPanelStyle(anchor: DOMRect | null): CSSProperties {
287
+ const insetRight = anchor
288
+ ? Math.max(16, window.innerWidth - anchor.right)
289
+ : 16;
290
+ const bottom = anchor
291
+ ? Math.max(52, window.innerHeight - anchor.top + 8)
292
+ : 52;
293
+ return {
294
+ position: "fixed",
295
+ right: `max(${insetRight}px, env(safe-area-inset-right, 0px))`,
296
+ bottom: `calc(${bottom}px + env(safe-area-inset-bottom, 0px))`,
297
+ zIndex: 2147483001,
298
+ width: "min(320px, calc(100vw - 32px))",
299
+ maxHeight: "min(420px, calc(100vh - 80px))",
300
+ overflow: "auto",
301
+ border: "1px solid #e4e4e7",
302
+ borderRadius: 12,
303
+ background: "#ffffff",
304
+ boxShadow: "0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06)",
305
+ };
306
+ }
@@ -0,0 +1,39 @@
1
+ /* packages/studio/src/studio-prose-ui.css
2
+ Document editor prose surface — full studio only, not the on-site widget. */
3
+
4
+ .prose-ui {
5
+ --p-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
6
+ --p-font-family-heading: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
7
+ --p-font-family-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
8
+ monospace;
9
+ --p-body-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
10
+ --p-h1-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
11
+ --p-h2-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
12
+ --p-h3-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
13
+ --p-h4-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
14
+ --p-h5-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
15
+ --p-h6-font-family: "Geist Sans", Inter, ui-sans-serif, system-ui, sans-serif;
16
+ }
17
+
18
+ .prose-ui img {
19
+ max-width: 100%;
20
+ height: auto;
21
+ }
22
+
23
+ .prose-ui > :first-child {
24
+ margin-top: 0;
25
+ }
26
+
27
+ .prose-ui > :last-child {
28
+ margin-bottom: 0;
29
+ }
30
+
31
+ .prose-ui [data-body-block] :is(p, h1, h2, h3, h4, h5, h6, ul, ol, blockquote) {
32
+ margin-top: 0;
33
+ margin-bottom: 0;
34
+ }
35
+
36
+ .prose-ui [data-body-block] :focus,
37
+ .prose-ui [data-body-block] :focus-visible {
38
+ outline: none;
39
+ }
@@ -101,10 +101,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
101
101
  [preference, resolved, setPreference, cyclePreference],
102
102
  );
103
103
 
104
+ const themeProps = stylex.props(theme, styles.root);
105
+
104
106
  return (
105
107
  <ThemeContext.Provider value={value}>
106
108
  <div
107
- {...stylex.props(theme, styles.root)}
109
+ {...themeProps}
110
+ className={[themeProps.className, "backmatter-studio"].filter(Boolean).join(" ")}
108
111
  >
109
112
  {children}
110
113
  </div>
@@ -97,16 +97,14 @@ export function isWorkspaceContentPath(pathname: string) {
97
97
  }
98
98
 
99
99
  /**
100
- * Where the content rail icon goes.
101
- * Already on the file tree: stay put (null).
102
- * On a document: the type's file tree.
100
+ * Where the content rail icon navigates.
101
+ * On a content route it stays put: a document toggles the file tree, and the
102
+ * type index is already the file tree.
103
103
  * Anywhere else (settings, media, connect): the workspace home, which opens the docs.
104
104
  */
105
105
  export function contentRailHref(pathname: string, slug: string): string | null {
106
106
  if (!isWorkspaceContentPath(pathname)) return workspaceHomePath(slug);
107
- const doc = pathname.match(/\/content\/([^/]+)\/([^/?]+)/);
108
- if (!doc) return null;
109
- return contentTypePath(slug, decodeURIComponent(doc[1]!));
107
+ return null;
110
108
  }
111
109
 
112
110
  export function isWorkspaceSettingsPath(pathname: string) {
@@ -122,3 +120,9 @@ export function isOrgPickerPath(pathname: string) {
122
120
  const path = pathname.replace(/\/$/, "") || "/";
123
121
  return path === "/invite";
124
122
  }
123
+
124
+ /** CLI approval is a full-screen form, not a workspace. */
125
+ export function isCliAuthorizePath(pathname: string) {
126
+ const path = pathname.replace(/\/$/, "") || "/";
127
+ return path === "/cli/authorize";
128
+ }