@backmatter/studio 0.0.8 → 0.0.9

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-b8qFsmA-.js +2401 -0
  2. package/dist/chunks/WorkspaceRoutes-BKKEkAq7.js +90126 -0
  3. package/dist/chunks/{dashboard-bNp6AbKE.js → dashboard-BNUcbYU0.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 +30896 -630
  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 +93 -49
  49. package/src/shell-resets.css +155 -0
  50. package/src/site-bar.tsx +1791 -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
package/src/site-bar.tsx CHANGED
@@ -1,70 +1,78 @@
1
1
  // packages/studio/src/site-bar.tsx
2
- // On-site bar: Live/Preview, the current page, the branch, then Agent and Publish.
3
- import { useEffect, useId, useRef, useState, type ReactNode } from "react";
4
- import { isRoutable, routeFor, type RouteModel } from "@backmatter/core";
2
+ // On-site bar. One mode button, the page, then Agent and Publish.
3
+ // Preview fills the bar from the button that was pressed and draws a frame
4
+ // from the bottom-right corner. The bar mounts in a shadow root.
5
+ import {
6
+ useCallback,
7
+ useEffect,
8
+ useId,
9
+ useLayoutEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ type MouseEvent,
14
+ type ReactNode,
15
+ type Ref,
16
+ } from "react";
17
+ import { createPortal } from "react-dom";
5
18
  import {
6
19
  checkoutBranch,
7
20
  createPreviewTicket,
8
- fetchDocs,
21
+ fetchDocNav,
9
22
  fetchGit,
10
- fetchSchema,
11
- type DocListItem,
23
+ fetchMe,
24
+ fetchWorkspace,
25
+ type PageNavItem,
12
26
  } from "./api";
13
- import { GitBranchIcon } from "./chrome/icons";
27
+ import { resolveDashboardOpenHref } from "./site-dashboard-href";
28
+ import { GearIcon, GitBranchIcon } from "./chrome/icons";
14
29
  import { matchAppHotkey, modifierSymbol } from "./chrome/hotkeys";
15
30
  import { PublishIcon, SparkIcon } from "./chat/icons";
16
- import { PREVIEW_CHROME_BORDER } from "./preview-frame";
31
+ import {
32
+ DEVBAR_EASE_DRAWER,
33
+ DEVBAR_EASE_IN,
34
+ DEVBAR_EASE_OUT,
35
+ DEVBAR_FLOOD_ENTER_MS,
36
+ DEVBAR_FLOOD_EXIT_MS,
37
+ HIGHLIGHT_BLOCKS_KEY,
38
+ PREVIEW_WITH_AGENT_KEY,
39
+ devbarChrome,
40
+ originWithin,
41
+ prefersReducedMotion,
42
+ readSiteTheme,
43
+ readStoredFlag,
44
+ readStoredMode,
45
+ rememberMode,
46
+ rememberStoredFlag,
47
+ resolveSiteTheme,
48
+ shouldAnimateWidth,
49
+ type DevbarActivity,
50
+ type DevbarMode,
51
+ type DevbarStep,
52
+ type SiteThemePreference,
53
+ } from "./devbar-state";
54
+ import { PREVIEW_CHROME_BORDER, PreviewFrame } from "./preview-frame";
17
55
 
18
56
  export const DEVBAR_CLASS = "backmatter-devbar";
19
57
 
20
58
  const DEFAULT_PREVIEW_ENTRY = "/api/backmatter/preview";
21
59
 
60
+ export type { DevbarActivity, DevbarStep };
61
+
22
62
  export type PageMenuDoc = {
23
63
  locale: string;
24
64
  type: string;
25
65
  slug: string;
26
66
  };
27
67
 
28
- export type PageMenuItem = {
29
- key: string;
30
- label: string;
31
- href: string;
32
- current: boolean;
33
- };
68
+ const PAGE_MENU_SIZE = 10;
69
+ const PAGE_MENU_WIDTH = 360;
34
70
 
35
- /** Routable documents, in label order. Unroutable models stay out of the menu. */
36
- export function pageMenuItems(input: {
37
- docs: readonly PageMenuDoc[];
38
- models: readonly RouteModel[];
39
- current: PageMenuDoc | null;
40
- }): PageMenuItem[] {
41
- const byType = new Map(input.models.map((model) => [model.type, model]));
42
- const locales = new Set(input.docs.map((doc) => doc.locale));
43
- const showLocale = locales.size > 1;
44
- const items: PageMenuItem[] = [];
45
- for (const doc of input.docs) {
46
- const model = byType.get(doc.type);
47
- if (!model || !isRoutable(model)) continue;
48
- const href = routeFor(model, doc.locale, doc.slug);
49
- if (!href) continue;
50
- const name = `${doc.type}/${doc.slug}`;
51
- items.push({
52
- key: `${doc.locale}/${doc.type}/${doc.slug}`,
53
- label: showLocale ? `${name} · ${doc.locale}` : name,
54
- href,
55
- current: Boolean(
56
- input.current &&
57
- input.current.locale === doc.locale &&
58
- input.current.type === doc.type &&
59
- input.current.slug === doc.slug,
60
- ),
61
- });
62
- }
63
- items.sort((a, b) => a.label.localeCompare(b.label));
64
- return items;
71
+ function pageMenuCacheKey(workspaceId: string, query: string) {
72
+ return `${workspaceId}:${query.trim().toLowerCase()}`;
65
73
  }
66
74
 
67
- type MenuState = "page" | "branch" | null;
75
+ type MenuState = "page" | "branch" | "settings" | null;
68
76
 
69
77
  export function SiteBar({
70
78
  label,
@@ -75,111 +83,607 @@ export function SiteBar({
75
83
  chatOpen,
76
84
  publishOpen,
77
85
  changeCount,
86
+ lineDiff,
87
+ activity = null,
78
88
  previewEntryPath = DEFAULT_PREVIEW_ENTRY,
79
89
  siteMode = "preview",
90
+ frame = false,
91
+ frameRadius,
92
+ entrance = "auto",
93
+ dashboardHref,
80
94
  onExit,
81
95
  onEnterPreview,
82
96
  onToggleChat,
83
97
  onTogglePublish,
98
+ onStop,
99
+ onViewLive,
100
+ onPublishedDone,
101
+ onSignOut,
102
+ onPublishAnchor,
84
103
  }: {
85
104
  label: string;
86
105
  branch?: string;
87
106
  tree?: string;
88
107
  doc: (PageMenuDoc & { workspaceId: string }) | null;
89
- siteMode?: "preview" | "live";
108
+ siteMode?: DevbarMode;
90
109
  exiting?: boolean;
91
110
  chatOpen: boolean;
92
111
  publishOpen: boolean;
93
112
  changeCount: number;
113
+ /** Insertions and deletions behind the preview changes row. */
114
+ lineDiff?: { added: number; removed: number };
115
+ activity?: DevbarActivity | null;
94
116
  previewEntryPath?: string;
117
+ /** Viewport frame. Hosts opt in with `devBar.previewFrame`. */
118
+ frame?: boolean;
119
+ /** Bottom corner radius for the preview ring (`devBar.previewFrameRadius`). */
120
+ frameRadius?: number;
121
+ /** `play` runs the rise on every mount. `auto` skips it when this tab was already in this mode. */
122
+ entrance?: "auto" | "play";
123
+ dashboardHref?: string;
95
124
  onExit: () => void;
96
125
  onEnterPreview?: () => void;
97
126
  onToggleChat: () => void;
98
127
  onTogglePublish: () => void;
128
+ onStop?: () => void;
129
+ onViewLive?: () => void;
130
+ /** Called 3.2s after the published row appears. */
131
+ onPublishedDone?: () => void;
132
+ /** Drops the site session. The settings menu shows Sign out when this is set. */
133
+ onSignOut?: () => void;
134
+ /** Light-DOM publish panel positions from the Deploy button. */
135
+ onPublishAnchor?: (rect: DOMRect | null) => void;
99
136
  }) {
100
- const rootRef = useRef<HTMLElement>(null);
137
+ const hostRef = useRef<HTMLDivElement>(null);
138
+ const rootRef = useRef<HTMLDivElement>(null);
139
+ const barRef = useRef<HTMLElement | null>(null);
140
+ const rowRef = useRef<HTMLDivElement>(null);
141
+ const modeBtnRef = useRef<HTMLButtonElement>(null);
142
+ const publishBtnRef = useRef<HTMLButtonElement>(null);
143
+ const settingsBtnRef = useRef<HTMLButtonElement>(null);
144
+ const checkRef = useRef<HTMLSpanElement>(null);
145
+ const [shadow, setShadow] = useState<ShadowRoot | null>(null);
146
+ const attachHost = useCallback((host: HTMLDivElement | null) => {
147
+ hostRef.current = host;
148
+ if (!host) {
149
+ setShadow(null);
150
+ return;
151
+ }
152
+ const root = host.shadowRoot ?? host.attachShadow({ mode: "open" });
153
+ setShadow((prev) => (prev === root ? prev : root));
154
+ }, []);
155
+ const [barNode, setBarNode] = useState<HTMLElement | null>(null);
101
156
  const [menu, setMenu] = useState<MenuState>(null);
157
+ const [mode, setMode] = useState<DevbarMode>(siteMode);
158
+ const [hidden, setHidden] = useState(false);
159
+ const hiddenRef = useRef(false);
160
+ const pending = useRef<DevbarMode | null>(null);
161
+ const widthRef = useRef(0);
162
+ const widthReady = useRef(false);
163
+ const rose = useRef(false);
164
+ const handlers = useRef({
165
+ toggleHidden: () => {},
166
+ onModeKey: (_origin: HTMLElement) => {},
167
+ onEscape: () => {},
168
+ openDashboard: () => false,
169
+ openShortcuts: () => {},
170
+ });
171
+ const previewWithAgent = useRef(true);
172
+ const [theme, setTheme] = useState<SiteThemePreference>("system");
173
+ const [highlightBlocks, setHighlightBlocks] = useState(true);
174
+ const [previewAgent, setPreviewAgent] = useState(true);
175
+ const [settingsPane, setSettingsPane] = useState<"main" | "shortcuts">("main");
176
+ const [hostName, setHostName] = useState("");
177
+ const [workspaceSlug, setWorkspaceSlug] = useState<string | null>(null);
178
+ const [workspaceDefaultLocale, setWorkspaceDefaultLocale] = useState("en");
179
+
180
+ useEffect(() => {
181
+ const id = doc?.workspaceId;
182
+ if (!id) {
183
+ setWorkspaceSlug(null);
184
+ return;
185
+ }
186
+ let cancelled = false;
187
+ void fetchWorkspace(id)
188
+ .then(({ workspace }) => {
189
+ if (cancelled) return;
190
+ setWorkspaceSlug(workspace.slug);
191
+ setWorkspaceDefaultLocale(workspace.locales[0] ?? "en");
192
+ })
193
+ .catch(() => {
194
+ if (!cancelled) setWorkspaceSlug(null);
195
+ });
196
+ return () => {
197
+ cancelled = true;
198
+ };
199
+ }, [doc?.workspaceId]);
200
+
201
+ const dashboardOpenHref = useMemo(
202
+ () => resolveDashboardOpenHref(dashboardHref, doc, workspaceSlug, workspaceDefaultLocale),
203
+ [dashboardHref, doc, workspaceSlug, workspaceDefaultLocale],
204
+ );
205
+
206
+ const setBar = useCallback((node: HTMLElement | null) => {
207
+ barRef.current = node;
208
+ if (node && !(node.getRootNode() instanceof ShadowRoot)) return;
209
+ setBarNode(node);
210
+ }, []);
211
+
212
+ const chrome = devbarChrome({ siteMode: mode, changeCount, activity });
213
+ const visualMode = chrome.mode;
214
+ const activityKey =
215
+ activity?.type === "editing"
216
+ ? `editing:${activity.field}`
217
+ : activity?.type === "publishing"
218
+ ? `publishing:${activity.steps.map((step) => `${step.state}:${step.label}`).join("|")}`
219
+ : activity?.type === "published"
220
+ ? `published:${activity.sha}`
221
+ : "";
222
+ const signature = `${mode}|${chrome.row}|${changeCount}|${lineDiff?.added ?? ""}|${lineDiff?.removed ?? ""}|${label}|${activityKey}`;
223
+
224
+ useEffect(() => {
225
+ setHostName(location.host);
226
+ const storage = browserStorage();
227
+ const nextTheme = readSiteTheme(storage);
228
+ setPreviewAgent(readStoredFlag(storage, PREVIEW_WITH_AGENT_KEY, true));
229
+ setTheme(nextTheme);
230
+ setHighlightBlocks(readStoredFlag(storage, HIGHLIGHT_BLOCKS_KEY, true));
231
+ }, []);
232
+
233
+ useEffect(() => {
234
+ applySiteTheme(theme);
235
+ if (theme !== "system" || typeof matchMedia !== "function") return;
236
+ const media = matchMedia("(prefers-color-scheme: dark)");
237
+ const onChange = () => applySiteTheme("system");
238
+ media.addEventListener("change", onChange);
239
+ return () => media.removeEventListener("change", onChange);
240
+ }, [theme]);
241
+
242
+ useEffect(() => {
243
+ document.documentElement.classList.toggle("backmatter-blocks", highlightBlocks);
244
+ return () => document.documentElement.classList.remove("backmatter-blocks");
245
+ }, [highlightBlocks]);
246
+
247
+ useEffect(() => {
248
+ const host = hostRef.current;
249
+ if (!host) return;
250
+ const root = document.documentElement;
251
+ const name = `${DEVBAR_CLASS}-pointing`;
252
+ const sync = () => host.classList.toggle(name, root.classList.contains("backmatter-pointing"));
253
+ sync();
254
+ const observer = new MutationObserver(sync);
255
+ observer.observe(root, { attributes: true, attributeFilter: ["class"] });
256
+ return () => observer.disconnect();
257
+ }, []);
258
+
259
+ useEffect(() => {
260
+ if (pending.current && pending.current !== siteMode && !exiting) {
261
+ pending.current = null;
262
+ rememberMode(siteMode);
263
+ setMode(siteMode);
264
+ return;
265
+ }
266
+ if (!pending.current) setMode(siteMode);
267
+ if (pending.current === siteMode) pending.current = null;
268
+ }, [siteMode, exiting]);
102
269
 
103
270
  useEffect(() => {
104
271
  if (!menu) return;
105
272
  function onPointer(event: PointerEvent) {
106
- const target = event.target;
107
- if (!(target instanceof Node)) return;
108
- if (rootRef.current?.contains(target)) return;
109
- setMenu(null);
110
- }
111
- function onKey(event: KeyboardEvent) {
112
- if (event.key !== "Escape") return;
273
+ const root = rootRef.current;
274
+ if (root && event.composedPath().includes(root)) return;
113
275
  setMenu(null);
114
276
  }
115
277
  document.addEventListener("pointerdown", onPointer);
116
- document.addEventListener("keydown", onKey);
278
+ return () => document.removeEventListener("pointerdown", onPointer);
279
+ }, [menu]);
280
+
281
+ useLayoutEffect(() => {
282
+ const bar = barRef.current;
283
+ const button = settingsBtnRef.current;
284
+ if (!bar || !button || menu !== "settings") return;
285
+ placeMenuRight(bar, button);
286
+ }, [menu, barNode, signature]);
287
+
288
+ useLayoutEffect(() => {
289
+ if (!onPublishAnchor) return;
290
+ const button = publishBtnRef.current;
291
+ if (!publishOpen || !button) {
292
+ onPublishAnchor(null);
293
+ return;
294
+ }
295
+ const report = () => onPublishAnchor(button.getBoundingClientRect());
296
+ report();
297
+ window.addEventListener("resize", report);
298
+ window.addEventListener("scroll", report, true);
117
299
  return () => {
118
- document.removeEventListener("pointerdown", onPointer);
119
- document.removeEventListener("keydown", onKey);
300
+ window.removeEventListener("resize", report);
301
+ window.removeEventListener("scroll", report, true);
120
302
  };
121
- }, [menu]);
303
+ }, [publishOpen, onPublishAnchor, signature, barNode]);
122
304
 
123
305
  useEffect(() => {
124
306
  function onKey(event: KeyboardEvent) {
125
- if (matchAppHotkey(event) !== "chat") return;
126
- setMenu(null);
307
+ if (isTypingTarget(event.composedPath()[0] ?? null)) return;
308
+ if ((event.metaKey || event.ctrlKey) && event.key === ".") {
309
+ event.preventDefault();
310
+ handlers.current.toggleHidden();
311
+ return;
312
+ }
313
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "p") {
314
+ event.preventDefault();
315
+ const button = modeBtnRef.current;
316
+ if (!button || button.disabled) return;
317
+ handlers.current.onModeKey(button);
318
+ return;
319
+ }
320
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "d") {
321
+ if (!handlers.current.openDashboard()) return;
322
+ event.preventDefault();
323
+ return;
324
+ }
325
+ if (event.key === "?" && !event.metaKey && !event.ctrlKey && !event.altKey) {
326
+ event.preventDefault();
327
+ handlers.current.openShortcuts();
328
+ return;
329
+ }
330
+ if (event.key === "Escape") {
331
+ handlers.current.onEscape();
332
+ return;
333
+ }
334
+ if (matchAppHotkey(event) === "chat") setMenu(null);
127
335
  }
128
336
  document.addEventListener("keydown", onKey, true);
129
337
  return () => document.removeEventListener("keydown", onKey, true);
130
338
  }, []);
131
339
 
132
- function dismissPanels() {
340
+ useLayoutEffect(() => {
341
+ const bar = barRef.current;
342
+ if (!shadow || !bar || !(bar.getRootNode() instanceof ShadowRoot)) return;
343
+
344
+ if (!rose.current) {
345
+ rose.current = true;
346
+ const settled = entrance !== "play" && readStoredMode() === siteMode;
347
+ if (entrance !== "play") rememberMode(siteMode);
348
+ if (settled) {
349
+ rootRef.current?.setAttribute("data-settled", "true");
350
+ requestAnimationFrame(() => {
351
+ requestAnimationFrame(() => rootRef.current?.removeAttribute("data-settled"));
352
+ });
353
+ } else if (prefersReducedMotion()) {
354
+ bar.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, easing: "ease", fill: "backwards" });
355
+ } else {
356
+ const rise = bar.animate(
357
+ [{ transform: "translateY(100%)" }, { transform: "none" }],
358
+ { duration: 460, easing: DEVBAR_EASE_OUT, delay: 600, fill: "backwards" },
359
+ );
360
+ rise.id = "rise";
361
+ if (rowRef.current) staggerChildren(rowRef.current, { delay: 740 });
362
+ }
363
+ }
364
+
365
+ const first = !widthReady.current;
366
+ widthReady.current = true;
367
+ const running = bar.getAnimations().some((animation) => animation.id === "w");
368
+ const before = running ? bar.offsetWidth : widthRef.current;
369
+ bar.getAnimations().filter((animation) => animation.id === "w").forEach((animation) => animation.cancel());
370
+ const after = bar.offsetWidth;
371
+ widthRef.current = after;
372
+ if (first) return;
373
+ const row = rowRef.current;
374
+ if (row) {
375
+ if (prefersReducedMotion()) {
376
+ row.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150, easing: "ease", fill: "backwards" });
377
+ } else {
378
+ staggerChildren(row, { delay: 40, blur: true });
379
+ }
380
+ }
381
+ if (prefersReducedMotion() || !shouldAnimateWidth(before, after)) return;
382
+ const width = bar.animate(
383
+ [{ width: `${before}px` }, { width: `${after}px` }],
384
+ { duration: 380, easing: DEVBAR_EASE_DRAWER },
385
+ );
386
+ width.id = "w";
387
+ }, [shadow, signature, entrance, siteMode]);
388
+
389
+ const publishedKey = activity?.type === "published" ? activity.sha : "";
390
+ useLayoutEffect(() => {
391
+ const check = checkRef.current;
392
+ if (!publishedKey || !check || prefersReducedMotion()) return;
393
+ check.animate(
394
+ [
395
+ { transform: "scale(0.5)", opacity: 0 },
396
+ { transform: "none", opacity: 1 },
397
+ ],
398
+ { duration: 420, delay: 140, easing: "cubic-bezier(.34, 1.56, .64, 1)", fill: "backwards" },
399
+ );
400
+ }, [publishedKey]);
401
+
402
+ useEffect(() => {
403
+ if (!publishedKey || !onPublishedDone) return;
404
+ const id = window.setTimeout(onPublishedDone, 3200);
405
+ return () => window.clearTimeout(id);
406
+ }, [publishedKey, onPublishedDone]);
407
+
408
+ function changeMode(next: DevbarMode, origin: HTMLElement) {
409
+ if (exiting || next === mode) return;
410
+ if (next === "preview" && !onEnterPreview) return;
411
+ const bar = barRef.current;
412
+ if (bar) {
413
+ const point = originWithin(bar.getBoundingClientRect(), origin.getBoundingClientRect());
414
+ bar.style.setProperty("--ox", `${point.x}px`);
415
+ bar.style.setProperty("--oy", `${point.y}px`);
416
+ }
417
+ if (next === "live") {
418
+ setMenu(null);
419
+ if (chatOpen) onToggleChat();
420
+ if (publishOpen) onTogglePublish();
421
+ }
422
+ pending.current = next;
423
+ rememberMode(next);
424
+ setMode(next);
425
+ if (next === "preview") onEnterPreview?.();
426
+ else onExit();
427
+ }
428
+
429
+ function setToolbarHidden(next: boolean) {
430
+ if (hiddenRef.current === next) return;
431
+ hiddenRef.current = next;
432
+ setMenu(null);
433
+ setHidden(next);
434
+ const bar = barRef.current;
435
+ if (!bar || !(bar.getRootNode() instanceof ShadowRoot)) return;
436
+ const activeRoot = bar.getRootNode();
437
+ const active = activeRoot instanceof ShadowRoot ? activeRoot.activeElement : null;
438
+ if (next && active instanceof HTMLElement) active.blur();
439
+ bar.getAnimations().filter((animation) => animation.id === "hide" || animation.id === "rise").forEach((animation) => animation.cancel());
440
+ if (prefersReducedMotion()) {
441
+ const fade = bar.animate(
442
+ [{ opacity: next ? 1 : 0 }, { opacity: next ? 0 : 1 }],
443
+ { duration: 200, easing: "ease", fill: "forwards" },
444
+ );
445
+ fade.id = next ? "hide" : "rise";
446
+ return;
447
+ }
448
+ if (next) {
449
+ const hide = bar.animate(
450
+ [{ transform: "translateY(0)" }, { transform: "translateY(100%)" }],
451
+ { duration: 220, easing: DEVBAR_EASE_IN, fill: "forwards" },
452
+ );
453
+ hide.id = "hide";
454
+ return;
455
+ }
456
+ const rise = bar.animate(
457
+ [{ transform: "translateY(100%)" }, { transform: "none" }],
458
+ { duration: 460, easing: DEVBAR_EASE_OUT, fill: "backwards" },
459
+ );
460
+ rise.id = "rise";
461
+ if (rowRef.current) staggerChildren(rowRef.current, { delay: 140 });
462
+ }
463
+
464
+ handlers.current.openDashboard = () => {
465
+ if (!dashboardHref) return false;
466
+ void (async () => {
467
+ let href = dashboardOpenHref ?? dashboardHref;
468
+ if (doc?.workspaceId && doc.type && doc.slug && !workspaceSlug) {
469
+ try {
470
+ const { workspace } = await fetchWorkspace(doc.workspaceId);
471
+ href =
472
+ resolveDashboardOpenHref(dashboardHref, doc, workspace.slug, workspace.locales[0] ?? "en") ??
473
+ href;
474
+ } catch {
475
+ // Keep the dashboard origin.
476
+ }
477
+ }
478
+ location.assign(href);
479
+ })();
480
+ return true;
481
+ };
482
+ handlers.current.openShortcuts = () => {
483
+ setSettingsPane("shortcuts");
484
+ setMenu("settings");
485
+ };
486
+ handlers.current.toggleHidden = () => setToolbarHidden(!hiddenRef.current);
487
+ handlers.current.onModeKey = (origin) => changeMode(visualMode === "preview" ? "live" : "preview", origin);
488
+ handlers.current.onEscape = () => {
489
+ if (menu) {
490
+ setMenu(null);
491
+ return;
492
+ }
493
+ if (publishOpen) {
494
+ onTogglePublish();
495
+ return;
496
+ }
497
+ if (activity?.type === "editing") onStop?.();
498
+ };
499
+
500
+ function openAgent(origin: HTMLElement) {
501
+ if (exiting) return;
502
+ setMenu(null);
503
+ if (publishOpen) onTogglePublish();
504
+ if (mode === "live" && previewWithAgent.current) changeMode("preview", origin);
505
+ onToggleChat();
506
+ }
507
+
508
+ function openPublish() {
509
+ if (exiting) return;
510
+ setMenu(null);
133
511
  if (chatOpen) onToggleChat();
512
+ onTogglePublish();
513
+ }
514
+
515
+ previewWithAgent.current = previewAgent;
516
+
517
+ function toggleSettings(pane: "main" | "shortcuts") {
134
518
  if (publishOpen) onTogglePublish();
519
+ setSettingsPane(pane);
520
+ setMenu((open) => (open === "settings" && pane === "main" ? null : "settings"));
135
521
  }
136
522
 
137
- function toggleMenu(next: Exclude<MenuState, null>) {
138
- setMenu((open) => {
139
- if (open === next) return null;
140
- dismissPanels();
141
- return next;
142
- });
523
+ function togglePageMenu() {
524
+ if (publishOpen) onTogglePublish();
525
+ setMenu((open) => (open === "page" ? null : "page"));
143
526
  }
144
527
 
145
- const publishEmphasis = publishOpen || changeCount > 0;
528
+ const modeDisabled = Boolean(exiting) || (visualMode === "live" && !onEnterPreview);
529
+ const publishDisabled = !doc || changeCount <= 0;
530
+ const modeButton = (
531
+ <ModeButton
532
+ mode={visualMode}
533
+ disabled={modeDisabled}
534
+ buttonRef={modeBtnRef}
535
+ onClick={(event) => changeMode(visualMode === "preview" ? "live" : "preview", event.currentTarget)}
536
+ />
537
+ );
146
538
 
147
- return (
148
- <>
149
- <style>{BAR_CSS}</style>
150
- <footer ref={rootRef} aria-label="Backmatter" className={DEVBAR_CLASS}>
151
- <div className={`${DEVBAR_CLASS}-mode`} role="group" aria-label="Site view">
152
- <button
153
- type="button"
154
- aria-pressed={siteMode === "live"}
155
- title="View the published site"
156
- disabled={exiting || siteMode === "live"}
157
- onClick={() => {
158
- setMenu(null);
159
- onExit();
160
- }}
161
- >
162
- Live
163
- </button>
164
- <button
165
- type="button"
166
- aria-pressed={siteMode === "preview"}
167
- title="View draft content"
168
- disabled={exiting || siteMode === "preview" || !onEnterPreview}
169
- onClick={() => {
170
- setMenu(null);
171
- onEnterPreview?.();
172
- }}
173
- >
174
- <Dot />
175
- Preview
176
- </button>
177
- </div>
539
+ let row: ReactNode;
540
+ if (activity?.type === "editing") {
541
+ row = (
542
+ <>
543
+ {modeButton}
544
+ <span className={`${DEVBAR_CLASS}-spin`} aria-hidden />
545
+ <span className={`${DEVBAR_CLASS}-shimmer`}>Agent is editing</span>
546
+ <code className={`${DEVBAR_CLASS}-field`}>{activity.field}</code>
547
+ <button type="button" className={`${DEVBAR_CLASS}-btn`} onClick={() => onStop?.()}>
548
+ Stop
549
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>esc</kbd>
550
+ </button>
551
+ </>
552
+ );
553
+ } else if (activity?.type === "publishing") {
554
+ row = (
555
+ <>
556
+ {modeButton}
557
+ <span className={`${DEVBAR_CLASS}-steps`}>
558
+ {activity.steps.map((step, index) => (
559
+ <span key={`${step.label}:${index}`} className={`${DEVBAR_CLASS}-step`}>
560
+ {index > 0 ? <span className={`${DEVBAR_CLASS}-dash`} aria-hidden>—</span> : null}
561
+ <StepMark state={step.state} />
562
+ <span>{step.label}</span>
563
+ </span>
564
+ ))}
565
+ </span>
566
+ </>
567
+ );
568
+ } else if (activity?.type === "published") {
569
+ row = (
570
+ <>
571
+ {modeButton}
572
+ <span ref={checkRef} className={`${DEVBAR_CLASS}-check`} aria-hidden>
573
+ ✓
574
+ </span>
575
+ <span className={`${DEVBAR_CLASS}-published`}>
576
+ Published <code className={`${DEVBAR_CLASS}-field`}>{activity.sha}</code>
577
+ <span className={`${DEVBAR_CLASS}-when`}> · {activity.when}</span>
578
+ </span>
579
+ <button type="button" className={`${DEVBAR_CLASS}-btn`} onClick={() => (onViewLive ?? onExit)()}>
580
+ View live
581
+ </button>
582
+ <AgentButton
583
+ pressed={chatOpen}
584
+ disabled={!doc}
585
+ shortcut={modifierSymbol()}
586
+ onClick={(event) => openAgent(event.currentTarget)}
587
+ />
588
+ </>
589
+ );
590
+ } else {
591
+ row = (
592
+ <>
593
+ {modeButton}
178
594
  <PageMenu
179
595
  label={label}
180
596
  doc={doc}
597
+ dashboardHref={dashboardOpenHref}
598
+ bar={barNode}
181
599
  open={menu === "page"}
182
- onToggle={() => toggleMenu("page")}
600
+ expanded={menu === "page"}
601
+ onToggle={togglePageMenu}
602
+ />
603
+ <span className={`${DEVBAR_CLASS}-rule`} aria-hidden />
604
+ <AgentButton
605
+ pressed={chatOpen}
606
+ disabled={!doc}
607
+ shortcut={modifierSymbol()}
608
+ onClick={(event) => openAgent(event.currentTarget)}
609
+ />
610
+ <PublishButton
611
+ buttonRef={publishBtnRef}
612
+ open={publishOpen}
613
+ count={changeCount}
614
+ disabled={publishDisabled}
615
+ onClick={openPublish}
616
+ />
617
+ <SettingsButton
618
+ buttonRef={settingsBtnRef}
619
+ open={menu === "settings"}
620
+ onClick={() => toggleSettings("main")}
621
+ />
622
+ </>
623
+ );
624
+ }
625
+
626
+ const body = (
627
+ <>
628
+ <style>{BAR_CSS}</style>
629
+ <div
630
+ ref={rootRef}
631
+ className={`${DEVBAR_CLASS}-root`}
632
+ data-mode={visualMode}
633
+ data-branch={branch || undefined}
634
+ >
635
+ {frame ? <PreviewFrame active={visualMode === "preview"} radius={frameRadius} /> : null}
636
+ <footer
637
+ ref={setBar}
638
+ aria-label="Backmatter"
639
+ aria-hidden={hidden || undefined}
640
+ inert={hidden ? true : undefined}
641
+ data-hidden={hidden ? "true" : "false"}
642
+ data-branch={branch || undefined}
643
+ className={DEVBAR_CLASS}
644
+ >
645
+ <div className={`${DEVBAR_CLASS}-clip`}>
646
+ <span className={`${DEVBAR_CLASS}-flood`} aria-hidden />
647
+ <div ref={rowRef} className={`${DEVBAR_CLASS}-row`}>
648
+ {row}
649
+ </div>
650
+ {activity?.type === "publishing" ? (
651
+ <span
652
+ className={`${DEVBAR_CLASS}-progress`}
653
+ style={{ width: `${Math.max(0, Math.min(1, activity.progress)) * 100}%` }}
654
+ aria-hidden
655
+ />
656
+ ) : null}
657
+ </div>
658
+ <div className={`${DEVBAR_CLASS}-sr`} aria-live="polite">
659
+ {chrome.announce}
660
+ </div>
661
+ </footer>
662
+ <SettingsMenu
663
+ open={menu === "settings"}
664
+ pane={settingsPane}
665
+ hostName={hostName}
666
+ branch={branch}
667
+ bar={barNode}
668
+ highlightBlocks={highlightBlocks}
669
+ previewAgent={previewAgent}
670
+ dashboardHref={dashboardOpenHref}
671
+ onHighlight={(on) => {
672
+ setHighlightBlocks(on);
673
+ rememberStoredFlag(browserStorage(), HIGHLIGHT_BLOCKS_KEY, on);
674
+ }}
675
+ onPreviewAgent={(on) => {
676
+ setPreviewAgent(on);
677
+ rememberStoredFlag(browserStorage(), PREVIEW_WITH_AGENT_KEY, on);
678
+ }}
679
+ onShortcuts={() => setSettingsPane("shortcuts")}
680
+ onBack={() => setSettingsPane("main")}
681
+ onDashboard={() => {
682
+ handlers.current.openDashboard();
683
+ }}
684
+ onHide={() => setToolbarHidden(true)}
685
+ onSwitchBranch={branch ? () => setMenu("branch") : undefined}
686
+ onSignOut={onSignOut}
183
687
  />
184
688
  {branch ? (
185
689
  <BranchMenu
@@ -187,161 +691,619 @@ export function SiteBar({
187
691
  tree={tree}
188
692
  workspaceId={doc?.workspaceId ?? null}
189
693
  previewEntryPath={previewEntryPath}
694
+ bar={barNode}
190
695
  open={menu === "branch"}
191
- onToggle={() => toggleMenu("branch")}
696
+ onClose={() => setMenu(null)}
192
697
  />
193
698
  ) : null}
194
- <span className={`${DEVBAR_CLASS}-rule`} aria-hidden />
195
- <div className={`${DEVBAR_CLASS}-actions`}>
196
- <button
197
- type="button"
198
- className={`${DEVBAR_CLASS}-btn ${DEVBAR_CLASS}-agent${chatOpen ? ` ${DEVBAR_CLASS}-on` : ""}`}
199
- aria-pressed={chatOpen}
200
- aria-keyshortcuts="Meta+J Control+J"
201
- disabled={!doc}
202
- onClick={() => {
203
- setMenu(null);
204
- if (publishOpen) onTogglePublish();
205
- onToggleChat();
206
- }}
207
- >
208
- <SparkIcon />
209
- <span>Agent</span>
210
- <kbd>{modifierSymbol()}J</kbd>
699
+ </div>
700
+ </>
701
+ );
702
+
703
+ return (
704
+ <div ref={attachHost} className={`${DEVBAR_CLASS}-host`} data-backmatter-devbar="">
705
+ <style>{LIGHT_CSS}</style>
706
+ {shadow ? createPortal(body, shadow) : <template shadowrootmode="open">{body}</template>}
707
+ </div>
708
+ );
709
+ }
710
+
711
+ function ModeButton({
712
+ mode,
713
+ disabled,
714
+ buttonRef,
715
+ onClick,
716
+ }: {
717
+ mode: DevbarMode;
718
+ disabled: boolean;
719
+ buttonRef: Ref<HTMLButtonElement>;
720
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
721
+ }) {
722
+ const preview = mode === "preview";
723
+ return (
724
+ <button
725
+ ref={buttonRef}
726
+ type="button"
727
+ data-mode-button=""
728
+ className={`${DEVBAR_CLASS}-mode`}
729
+ aria-pressed={preview}
730
+ aria-keyshortcuts="Meta+Shift+P Control+Shift+P"
731
+ title={preview ? "View the published site" : "View draft content"}
732
+ disabled={disabled}
733
+ onClick={onClick}
734
+ >
735
+ <ModeMark exit={preview} />
736
+ {preview ? "Exit preview" : "Enter preview"}
737
+ </button>
738
+ );
739
+ }
740
+
741
+ function AgentButton({
742
+ pressed,
743
+ disabled,
744
+ shortcut,
745
+ onClick,
746
+ }: {
747
+ pressed: boolean;
748
+ disabled: boolean;
749
+ shortcut: string;
750
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
751
+ }) {
752
+ return (
753
+ <button
754
+ type="button"
755
+ className={`${DEVBAR_CLASS}-btn ${DEVBAR_CLASS}-agent${pressed ? ` ${DEVBAR_CLASS}-on` : ""}`}
756
+ aria-pressed={pressed}
757
+ aria-keyshortcuts="Meta+J Control+J"
758
+ disabled={disabled}
759
+ onClick={onClick}
760
+ >
761
+ <SparkIcon />
762
+ <span>Agent</span>
763
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>{shortcut}J</kbd>
764
+ </button>
765
+ );
766
+ }
767
+
768
+ function PublishButton({
769
+ buttonRef,
770
+ open,
771
+ count,
772
+ disabled,
773
+ onClick,
774
+ }: {
775
+ buttonRef?: Ref<HTMLButtonElement>;
776
+ open?: boolean;
777
+ count: number;
778
+ disabled: boolean;
779
+ onClick: () => void;
780
+ }) {
781
+ return (
782
+ <button
783
+ ref={buttonRef}
784
+ type="button"
785
+ className={`${DEVBAR_CLASS}-publish${open ? ` ${DEVBAR_CLASS}-publish-on` : ""}`}
786
+ aria-haspopup="dialog"
787
+ aria-expanded={open ?? false}
788
+ disabled={disabled}
789
+ onClick={onClick}
790
+ >
791
+ <PublishIcon />
792
+ <span>
793
+ Publish
794
+ {count > 0 ? <span className={`${DEVBAR_CLASS}-count`}> {count}</span> : null}
795
+ </span>
796
+ </button>
797
+ );
798
+ }
799
+
800
+ function ModeMark({ exit }: { exit: boolean }) {
801
+ if (exit) {
802
+ return (
803
+ <svg className={`${DEVBAR_CLASS}-mark`} width="12" height="12" viewBox="0 0 12 12" aria-hidden>
804
+ <path d="M3 3l6 6M9 3 3 9" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
805
+ </svg>
806
+ );
807
+ }
808
+ return (
809
+ <svg className={`${DEVBAR_CLASS}-mark`} width="12" height="12" viewBox="0 0 12 12" aria-hidden>
810
+ <circle cx="6" cy="6" r="3.25" stroke="currentColor" strokeWidth="1.25" />
811
+ <circle cx="6" cy="6" r="1.35" fill="currentColor" />
812
+ </svg>
813
+ );
814
+ }
815
+
816
+ function StepMark({ state }: { state: DevbarStep["state"] }) {
817
+ if (state === "done") return <span className={`${DEVBAR_CLASS}-done`} aria-hidden>✓</span>;
818
+ if (state === "active") return <span className={`${DEVBAR_CLASS}-spin`} aria-hidden />;
819
+ return <span className={`${DEVBAR_CLASS}-ring`} aria-hidden />;
820
+ }
821
+
822
+ function staggerChildren(row: HTMLElement, options: { delay?: number; blur?: boolean } = {}) {
823
+ const delay = options.delay ?? 0;
824
+ if (prefersReducedMotion()) {
825
+ row.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150, easing: "ease", fill: "backwards" });
826
+ return;
827
+ }
828
+ [...row.children].forEach((el, index) => {
829
+ (el as HTMLElement).animate(
830
+ [
831
+ { opacity: 0, transform: "translateY(4px)", filter: options.blur ? "blur(2px)" : "none" },
832
+ { opacity: 1, transform: "none", filter: "none" },
833
+ ],
834
+ { duration: 300, delay: delay + index * 28, easing: DEVBAR_EASE_OUT, fill: "backwards" },
835
+ );
836
+ });
837
+ }
838
+
839
+ function isTypingTarget(target: EventTarget | null) {
840
+ if (!(target instanceof HTMLElement)) return false;
841
+ const tag = target.tagName;
842
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
843
+ return target.isContentEditable;
844
+ }
845
+
846
+ function SettingsButton({
847
+ buttonRef,
848
+ open,
849
+ onClick,
850
+ }: {
851
+ buttonRef?: Ref<HTMLButtonElement>;
852
+ open: boolean;
853
+ onClick: () => void;
854
+ }) {
855
+ return (
856
+ <button
857
+ ref={buttonRef}
858
+ type="button"
859
+ className={`${DEVBAR_CLASS}-gear${open ? ` ${DEVBAR_CLASS}-gear-on` : ""}`}
860
+ aria-label="Settings"
861
+ aria-haspopup="dialog"
862
+ aria-expanded={open}
863
+ onClick={onClick}
864
+ >
865
+ <GearIcon />
866
+ </button>
867
+ );
868
+ }
869
+
870
+ function SettingsMenu({
871
+ open,
872
+ pane,
873
+ hostName,
874
+ branch,
875
+ bar,
876
+ highlightBlocks,
877
+ previewAgent,
878
+ dashboardHref,
879
+ onHighlight,
880
+ onPreviewAgent,
881
+ onShortcuts,
882
+ onBack,
883
+ onDashboard,
884
+ onHide,
885
+ onSwitchBranch,
886
+ onSignOut,
887
+ }: {
888
+ open: boolean;
889
+ pane: "main" | "shortcuts";
890
+ hostName: string;
891
+ branch?: string;
892
+ bar: HTMLElement | null;
893
+ highlightBlocks: boolean;
894
+ previewAgent: boolean;
895
+ dashboardHref?: string;
896
+ onHighlight: (on: boolean) => void;
897
+ onPreviewAgent: (on: boolean) => void;
898
+ onShortcuts: () => void;
899
+ onBack: () => void;
900
+ onDashboard: () => void;
901
+ onHide: () => void;
902
+ onSwitchBranch?: () => void;
903
+ onSignOut?: () => void;
904
+ }) {
905
+ const titleId = useId();
906
+ const [email, setEmail] = useState<string | null>(null);
907
+ const shortcut = modifierSymbol();
908
+
909
+ useEffect(() => {
910
+ if (!open) return;
911
+ let cancelled = false;
912
+ void fetchMe()
913
+ .then((me) => {
914
+ if (!cancelled) setEmail(me?.user.email ?? null);
915
+ })
916
+ .catch(() => {
917
+ if (!cancelled) setEmail(null);
918
+ });
919
+ return () => {
920
+ cancelled = true;
921
+ };
922
+ }, [open]);
923
+
924
+ if (!open || !bar) return null;
925
+ const place = hostName && branch ? `${hostName} · ${branch}` : hostName || branch || "";
926
+ const panel =
927
+ pane === "shortcuts" ? (
928
+ <div role="dialog" aria-labelledby={titleId} className={`${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-settings`}>
929
+ <div className={`${DEVBAR_CLASS}-settings-head`}>
930
+ <button type="button" className={`${DEVBAR_CLASS}-settings-back`} onClick={onBack}>
931
+ <Chevron />
932
+ <span id={titleId}>Shortcuts</span>
211
933
  </button>
212
- <button
213
- type="button"
214
- className={`${DEVBAR_CLASS}-btn${publishEmphasis ? ` ${DEVBAR_CLASS}-on` : ""}`}
215
- aria-pressed={publishOpen}
216
- disabled={!doc}
217
- onClick={() => {
218
- setMenu(null);
219
- if (chatOpen) onToggleChat();
220
- onTogglePublish();
221
- }}
222
- >
223
- <PublishIcon />
224
- <span>
225
- Publish
226
- {changeCount ? <span className={`${DEVBAR_CLASS}-count`}> {changeCount}</span> : null}
934
+ </div>
935
+ <ShortcutRow label="Toggle preview" keys={`${shortcut}⇧P`} />
936
+ <ShortcutRow label="Agent" keys={`${shortcut}J`} />
937
+ <ShortcutRow label="Open in dashboard" keys={`${shortcut}D`} />
938
+ <ShortcutRow label="Hide toolbar" keys={`${shortcut}.`} />
939
+ <ShortcutRow label="Keyboard shortcuts" keys="?" />
940
+ </div>
941
+ ) : (
942
+ <div role="dialog" aria-labelledby={titleId} className={`${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-settings`}>
943
+ <div className={`${DEVBAR_CLASS}-settings-head`}>
944
+ <span id={titleId} className={`${DEVBAR_CLASS}-settings-kicker`}>
945
+ Settings
946
+ </span>
947
+ {place ? <span className={`${DEVBAR_CLASS}-settings-place`}>{place}</span> : null}
948
+ </div>
949
+ <div className={`${DEVBAR_CLASS}-settings-row`}>
950
+ <BlocksIcon />
951
+ <span>Highlight editable blocks</span>
952
+ <Switch on={highlightBlocks} label="Highlight editable blocks" onClick={() => onHighlight(!highlightBlocks)} />
953
+ </div>
954
+ <div className={`${DEVBAR_CLASS}-settings-row`}>
955
+ <EyeIcon />
956
+ <span>Preview when Agent edits</span>
957
+ <Switch on={previewAgent} label="Preview when Agent edits" onClick={() => onPreviewAgent(!previewAgent)} />
958
+ </div>
959
+ <div className={`${DEVBAR_CLASS}-settings-rule`} />
960
+ {dashboardHref ? (
961
+ <button type="button" className={`${DEVBAR_CLASS}-settings-action`} onClick={onDashboard}>
962
+ <GridIcon />
963
+ <span>Open in dashboard</span>
964
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>{shortcut}D</kbd>
965
+ </button>
966
+ ) : null}
967
+ {onSwitchBranch ? (
968
+ <button type="button" className={`${DEVBAR_CLASS}-settings-action`} onClick={onSwitchBranch}>
969
+ <span className={`${DEVBAR_CLASS}-settings-icon`} aria-hidden>
970
+ <GitBranchIcon />
227
971
  </span>
972
+ <span>Switch branch</span>
973
+ {branch ? <span className={`${DEVBAR_CLASS}-settings-email`}>{branch}</span> : null}
228
974
  </button>
229
- </div>
230
- </footer>
231
- </>
975
+ ) : null}
976
+ <button type="button" className={`${DEVBAR_CLASS}-settings-action`} onClick={onShortcuts}>
977
+ <KeyboardIcon />
978
+ <span>Keyboard shortcuts</span>
979
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>?</kbd>
980
+ </button>
981
+ <button type="button" className={`${DEVBAR_CLASS}-settings-action`} aria-keyshortcuts="Meta+. Control+." onClick={onHide}>
982
+ <HideIcon />
983
+ <span>Hide toolbar</span>
984
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>{shortcut}.</kbd>
985
+ </button>
986
+ {onSignOut ? (
987
+ <>
988
+ <div className={`${DEVBAR_CLASS}-settings-rule`} />
989
+ <button type="button" className={`${DEVBAR_CLASS}-settings-action`} onClick={onSignOut}>
990
+ <LeaveIcon />
991
+ <span>Sign out</span>
992
+ {email ? <span className={`${DEVBAR_CLASS}-settings-email`}>{email}</span> : null}
993
+ </button>
994
+ </>
995
+ ) : null}
996
+ </div>
997
+ );
998
+ return createPortal(panel, bar);
999
+ }
1000
+
1001
+ function ShortcutRow({ label, keys }: { label: string; keys: string }) {
1002
+ return (
1003
+ <div className={`${DEVBAR_CLASS}-settings-action ${DEVBAR_CLASS}-settings-static`}>
1004
+ <span>{label}</span>
1005
+ <kbd className={`${DEVBAR_CLASS}-kbd`}>{keys}</kbd>
1006
+ </div>
232
1007
  );
233
1008
  }
234
1009
 
235
- function Dot() {
236
- return <span className={`${DEVBAR_CLASS}-dot`} aria-hidden />;
1010
+ function Switch({ on, label, onClick }: { on: boolean; label: string; onClick: () => void }) {
1011
+ return (
1012
+ <button
1013
+ type="button"
1014
+ role="switch"
1015
+ className={`${DEVBAR_CLASS}-switch${on ? ` ${DEVBAR_CLASS}-switch-on` : ""}`}
1016
+ aria-checked={on}
1017
+ aria-label={label}
1018
+ onClick={onClick}
1019
+ >
1020
+ <span className={`${DEVBAR_CLASS}-knob`} />
1021
+ </button>
1022
+ );
237
1023
  }
238
1024
 
239
- function Chevron() {
1025
+ function placeMenuRight(bar: HTMLElement, button: HTMLElement) {
1026
+ const barRect = bar.getBoundingClientRect();
1027
+ const btnRect = button.getBoundingClientRect();
1028
+ bar.style.setProperty("--menu-right", `${Math.max(0, barRect.right - btnRect.right)}px`);
1029
+ }
1030
+
1031
+ function placeMenuCenter(bar: HTMLElement, button: HTMLElement) {
1032
+ const barRect = bar.getBoundingClientRect();
1033
+ const btnRect = button.getBoundingClientRect();
1034
+ const menuWidth = Math.min(PAGE_MENU_WIDTH, window.innerWidth - 32);
1035
+ const anchor = btnRect.left + btnRect.width / 2 - barRect.left;
1036
+ let left = anchor - menuWidth / 2;
1037
+ const inset = 16;
1038
+ const minLeft = inset - barRect.left;
1039
+ const maxLeft = window.innerWidth - inset - menuWidth - barRect.left;
1040
+ bar.style.setProperty("--menu-x", `${Math.max(minLeft, Math.min(left, maxLeft))}px`);
1041
+ }
1042
+
1043
+ function BlocksIcon() {
240
1044
  return (
241
- <svg className={`${DEVBAR_CLASS}-chevron`} width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden>
242
- <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
1045
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1046
+ <path d="M3 3h4.2v4.2H3zM8.8 3H13v4.2H8.8zM3 8.8h4.2V13H3z" fill="none" stroke="currentColor" strokeWidth="1.3" />
1047
+ <path d="M8.8 10.2h4.2" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeDasharray="1.4 1.5" />
1048
+ </svg>
1049
+ );
1050
+ }
1051
+
1052
+ function EyeIcon() {
1053
+ return (
1054
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1055
+ <path d="M1.6 8S4 3.8 8 3.8 14.4 8 14.4 8 12 12.2 8 12.2 1.6 8 1.6 8Z" fill="none" stroke="currentColor" strokeWidth="1.3" />
1056
+ <circle cx="8" cy="8" r="1.8" fill="none" stroke="currentColor" strokeWidth="1.3" />
1057
+ </svg>
1058
+ );
1059
+ }
1060
+
1061
+ function GridIcon() {
1062
+ return (
1063
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1064
+ <path d="M2.5 2.5h4.2v4.2H2.5zM9.3 2.5h4.2v4.2H9.3zM2.5 9.3h4.2v4.2H2.5zM9.3 9.3h4.2v4.2H9.3z" fill="none" stroke="currentColor" strokeWidth="1.3" />
1065
+ </svg>
1066
+ );
1067
+ }
1068
+
1069
+ function KeyboardIcon() {
1070
+ return (
1071
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1072
+ <rect x="1.8" y="3.6" width="12.4" height="8.8" rx="1.4" fill="none" stroke="currentColor" strokeWidth="1.3" />
1073
+ <path d="M4 6.2h.1M6.4 6.2h.1M8.8 6.2h.1M11.2 6.2h.1M4.8 8.4h6.4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
1074
+ </svg>
1075
+ );
1076
+ }
1077
+
1078
+ function HideIcon() {
1079
+ return (
1080
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1081
+ <path d="M2 8s2.2-3.6 6-3.6S14 8 14 8s-2.2 3.6-6 3.6S2 8 2 8Z" fill="none" stroke="currentColor" strokeWidth="1.3" />
1082
+ <path d="M3 13 13 3" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
243
1083
  </svg>
244
1084
  );
245
1085
  }
246
1086
 
1087
+ function LeaveIcon() {
1088
+ return (
1089
+ <svg className={`${DEVBAR_CLASS}-settings-icon`} width="16" height="16" viewBox="0 0 16 16" aria-hidden>
1090
+ <path d="M6.5 3.2H3.8v9.6h2.7" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
1091
+ <path d="M7 8h6.2M11 5.4 13.6 8 11 10.6" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" />
1092
+ </svg>
1093
+ );
1094
+ }
1095
+
1096
+ function browserStorage(): Storage | null {
1097
+ try {
1098
+ return typeof localStorage === "undefined" ? null : localStorage;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+
1104
+ function applySiteTheme(preference: SiteThemePreference) {
1105
+ if (typeof document === "undefined") return;
1106
+ const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
1107
+ const dark = resolveSiteTheme(preference, prefersDark) === "dark";
1108
+ const root = document.documentElement;
1109
+ root.dataset.theme = dark ? "dark" : "light";
1110
+ root.style.colorScheme = preference === "system" ? "light dark" : dark ? "dark" : "light";
1111
+ root.classList.toggle("dark", dark);
1112
+ root.classList.toggle("light", !dark);
1113
+ }
1114
+
247
1115
  function PageMenu({
248
1116
  label,
249
1117
  doc,
1118
+ dashboardHref,
1119
+ bar,
250
1120
  open,
1121
+ expanded,
251
1122
  onToggle,
252
1123
  }: {
253
1124
  label: string;
254
1125
  doc: (PageMenuDoc & { workspaceId: string }) | null;
1126
+ dashboardHref?: string;
1127
+ bar: HTMLElement | null;
255
1128
  open: boolean;
1129
+ expanded: boolean;
256
1130
  onToggle: () => void;
257
1131
  }) {
258
- const [docs, setDocs] = useState<DocListItem[] | null>(null);
259
- const [models, setModels] = useState<RouteModel[] | null>(null);
1132
+ const [rows, setRows] = useState<PageNavItem[]>([]);
1133
+ const [total, setTotal] = useState(0);
1134
+ const [loading, setLoading] = useState(false);
1135
+ const [loadingMore, setLoadingMore] = useState(false);
260
1136
  const [error, setError] = useState<string | null>(null);
261
1137
  const [query, setQuery] = useState("");
262
1138
  const listId = useId();
1139
+ const buttonRef = useRef<HTMLButtonElement>(null);
1140
+ const menuRef = useRef<HTMLDivElement>(null);
1141
+ const filterRef = useRef<HTMLInputElement>(null);
1142
+ const requestRef = useRef(0);
1143
+ const cacheRef = useRef(new Map<string, { rows: PageNavItem[]; total: number }>());
263
1144
 
264
1145
  useEffect(() => {
265
- if (!open) setQuery("");
1146
+ if (open) return;
1147
+ setQuery("");
266
1148
  }, [open]);
267
1149
 
268
- useEffect(() => {
269
- if (!open || !doc || docs) return;
270
- let cancelled = false;
271
- void Promise.all([fetchDocs(doc.workspaceId), fetchSchema(doc.workspaceId)])
272
- .then(([listed, schema]) => {
273
- if (cancelled) return;
274
- setDocs(listed.docs);
275
- setModels(
276
- (schema.schema?.payload.models ?? []).map((model) => ({
277
- type: model.type,
278
- ...(model.route !== undefined ? { route: model.route } : {}),
279
- ...(model.indexSlug ? { indexSlug: model.indexSlug } : {}),
280
- })),
281
- );
1150
+ useLayoutEffect(() => {
1151
+ if (!open) return;
1152
+ filterRef.current?.focus({ preventScroll: true });
1153
+ }, [open]);
1154
+
1155
+ useLayoutEffect(() => {
1156
+ const button = buttonRef.current;
1157
+ if (!open || !bar || !button) return;
1158
+ const place = () => placeMenuCenter(bar, button);
1159
+ place();
1160
+ window.addEventListener("resize", place);
1161
+ return () => window.removeEventListener("resize", place);
1162
+ }, [open, bar]);
1163
+
1164
+ const loadPage = useCallback(
1165
+ (offset: number, append: boolean, options?: { silent?: boolean }) => {
1166
+ if (!doc) return;
1167
+ const requestId = ++requestRef.current;
1168
+ const q = query.trim();
1169
+ const cacheKey = pageMenuCacheKey(doc.workspaceId, q);
1170
+ if (append) setLoadingMore(true);
1171
+ else if (!options?.silent) setLoading(true);
1172
+ void fetchDocNav(doc.workspaceId, {
1173
+ q: q || undefined,
1174
+ limit: PAGE_MENU_SIZE,
1175
+ offset,
1176
+ current: doc,
282
1177
  })
283
- .catch(() => {
284
- if (!cancelled) setError("Couldn't load pages");
285
- });
286
- return () => {
287
- cancelled = true;
288
- };
289
- }, [open, doc, docs]);
1178
+ .then((page) => {
1179
+ if (requestId !== requestRef.current) return;
1180
+ setTotal(page.total);
1181
+ setRows((prev) => {
1182
+ const next = append ? [...prev, ...page.items] : page.items;
1183
+ cacheRef.current.set(cacheKey, { rows: next, total: page.total });
1184
+ return next;
1185
+ });
1186
+ setError(null);
1187
+ })
1188
+ .catch(() => {
1189
+ if (requestId !== requestRef.current) return;
1190
+ if (!append) {
1191
+ const hit = cacheRef.current.get(cacheKey);
1192
+ if (!hit) setRows([]);
1193
+ }
1194
+ setError("Couldn't load pages");
1195
+ })
1196
+ .finally(() => {
1197
+ if (requestId !== requestRef.current) return;
1198
+ setLoading(false);
1199
+ setLoadingMore(false);
1200
+ });
1201
+ },
1202
+ [doc, query],
1203
+ );
290
1204
 
291
- const items =
292
- docs && models ? pageMenuItems({ docs, models, current: doc }) : null;
293
- const shown = items ? filterItems(items, query) : null;
1205
+ useEffect(() => {
1206
+ if (!open || !doc) return;
1207
+ const q = query.trim();
1208
+ const cacheKey = pageMenuCacheKey(doc.workspaceId, q);
1209
+ const hit = cacheRef.current.get(cacheKey);
1210
+ if (hit) {
1211
+ setRows(hit.rows);
1212
+ setTotal(hit.total);
1213
+ setError(null);
1214
+ }
1215
+ const timer = window.setTimeout(
1216
+ () => loadPage(0, false, { silent: Boolean(hit) }),
1217
+ q ? 200 : 0,
1218
+ );
1219
+ return () => window.clearTimeout(timer);
1220
+ }, [open, doc, query, loadPage]);
294
1221
 
295
- if (!doc) {
296
- return <span className={`${DEVBAR_CLASS}-crumb`}>{label}</span>;
1222
+ function onMenuScroll() {
1223
+ const menu = menuRef.current;
1224
+ if (!menu || loading || loadingMore || rows.length >= total) return;
1225
+ if (menu.scrollTop + menu.clientHeight < menu.scrollHeight - 48) return;
1226
+ loadPage(rows.length, true);
297
1227
  }
298
1228
 
1229
+ if (!doc) return <span className={`${DEVBAR_CLASS}-crumb`}>{label}</span>;
1230
+
1231
+ const empty = !loading && !error && rows.length === 0;
1232
+ const menuNode = open ? (
1233
+ <div
1234
+ id={listId}
1235
+ role="menu"
1236
+ aria-label="Pages"
1237
+ className={`${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-page-menu`}
1238
+ >
1239
+ <MenuBody
1240
+ query={query}
1241
+ onQuery={setQuery}
1242
+ inputRef={filterRef}
1243
+ searchClassName={`${DEVBAR_CLASS}-page-search`}
1244
+ showFilter
1245
+ empty={empty}
1246
+ emptyLabel={query.trim() ? "Nothing matches" : "No pages"}
1247
+ error={error}
1248
+ loading={loading && rows.length === 0}
1249
+ />
1250
+ <div ref={menuRef} className={`${DEVBAR_CLASS}-page-menu-list`} onScroll={onMenuScroll}>
1251
+ {rows.map((item) => (
1252
+ <button
1253
+ key={item.key}
1254
+ type="button"
1255
+ role="menuitem"
1256
+ className={`${DEVBAR_CLASS}-option${item.current ? ` ${DEVBAR_CLASS}-current` : ""}`}
1257
+ aria-current={item.current ? "page" : undefined}
1258
+ onClick={() => {
1259
+ if (item.current || item.href === location.pathname) {
1260
+ onToggle();
1261
+ return;
1262
+ }
1263
+ location.assign(item.href);
1264
+ }}
1265
+ >
1266
+ <span>{item.label}</span>
1267
+ </button>
1268
+ ))}
1269
+ {loadingMore ? <p className={`${DEVBAR_CLASS}-note`}>Loading more…</p> : null}
1270
+ {empty && !error ? <p className={`${DEVBAR_CLASS}-note`}>{query.trim() ? "Nothing matches" : "No pages"}</p> : null}
1271
+ {error ? <p className={`${DEVBAR_CLASS}-note ${DEVBAR_CLASS}-note-error`}>{error}</p> : null}
1272
+ {loading && rows.length === 0 ? (
1273
+ <p className={`${DEVBAR_CLASS}-note ${DEVBAR_CLASS}-note-muted`}>Loading pages…</p>
1274
+ ) : null}
1275
+ </div>
1276
+ {dashboardHref ? (
1277
+ <div className={`${DEVBAR_CLASS}-page-menu-foot`}>
1278
+ <button
1279
+ type="button"
1280
+ role="menuitem"
1281
+ className={`${DEVBAR_CLASS}-option`}
1282
+ onClick={() => location.assign(dashboardHref)}
1283
+ >
1284
+ <span>Open in dashboard</span>
1285
+ </button>
1286
+ </div>
1287
+ ) : null}
1288
+ </div>
1289
+ ) : null;
1290
+
299
1291
  return (
300
- <div className={`${DEVBAR_CLASS}-pop`}>
1292
+ <>
301
1293
  <button
1294
+ ref={buttonRef}
302
1295
  type="button"
303
- className={`${DEVBAR_CLASS}-crumb ${DEVBAR_CLASS}-page`}
1296
+ className={`${DEVBAR_CLASS}-btn ${DEVBAR_CLASS}-page${expanded ? ` ${DEVBAR_CLASS}-page-on` : ""}`}
304
1297
  aria-haspopup="menu"
305
- aria-expanded={open}
1298
+ aria-expanded={expanded}
306
1299
  aria-controls={listId}
307
1300
  onClick={onToggle}
308
1301
  >
309
1302
  <span>{label}</span>
310
- <Chevron />
311
- </button>
312
- {open ? (
313
- <div id={listId} role="menu" aria-label="Pages" className={`${DEVBAR_CLASS}-menu`}>
314
- <MenuBody
315
- query={query}
316
- onQuery={setQuery}
317
- showFilter={(items?.length ?? 0) > 7}
318
- empty={shown?.length === 0}
319
- emptyLabel={query.trim() ? "Nothing matches" : "No pages"}
320
- error={error}
321
- loading={shown === null && !error}
322
- >
323
- {shown?.map((item) => (
324
- <button
325
- key={item.key}
326
- type="button"
327
- role="menuitem"
328
- className={`${DEVBAR_CLASS}-option${item.current ? ` ${DEVBAR_CLASS}-current` : ""}`}
329
- aria-current={item.current ? "page" : undefined}
330
- onClick={() => {
331
- if (item.current || item.href === location.pathname) {
332
- onToggle();
333
- return;
334
- }
335
- location.assign(item.href);
336
- }}
337
- >
338
- <span>{item.label}</span>
339
- </button>
340
- ))}
341
- </MenuBody>
342
- </div>
343
- ) : null}
344
- </div>
1303
+ <Chevron />
1304
+ </button>
1305
+ {menuNode && bar ? createPortal(menuNode, bar) : null}
1306
+ </>
345
1307
  );
346
1308
  }
347
1309
 
@@ -350,15 +1312,17 @@ function BranchMenu({
350
1312
  tree,
351
1313
  workspaceId,
352
1314
  previewEntryPath,
1315
+ bar,
353
1316
  open,
354
- onToggle,
1317
+ onClose,
355
1318
  }: {
356
1319
  branch: string;
357
1320
  tree?: string;
358
1321
  workspaceId: string | null;
359
1322
  previewEntryPath: string;
1323
+ bar: HTMLElement | null;
360
1324
  open: boolean;
361
- onToggle: () => void;
1325
+ onClose: () => void;
362
1326
  }) {
363
1327
  const [names, setNames] = useState<string[] | null>(null);
364
1328
  const [defaultBranch, setDefaultBranch] = useState<string | null>(null);
@@ -390,87 +1354,66 @@ function BranchMenu({
390
1354
  };
391
1355
  }, [open, workspaceId, names, branch]);
392
1356
 
393
- const shown = names ? filterItems(names.map((name) => ({ key: name, label: name })), query) : null;
394
-
395
- if (!workspaceId) {
396
- return (
397
- <span className={`${DEVBAR_CLASS}-crumb ${DEVBAR_CLASS}-branch`}>
398
- <GitBranchIcon />
399
- <span>{branch}</span>
400
- </span>
401
- );
402
- }
1357
+ if (!open || !bar) return null;
403
1358
 
404
- return (
405
- <div className={`${DEVBAR_CLASS}-pop`}>
406
- <button
407
- type="button"
408
- className={`${DEVBAR_CLASS}-crumb ${DEVBAR_CLASS}-branch`}
409
- aria-haspopup="menu"
410
- aria-expanded={open}
411
- aria-controls={listId}
412
- aria-label={`Branch ${branch}`}
413
- aria-busy={busy}
414
- disabled={busy}
415
- onClick={onToggle}
416
- >
417
- <GitBranchIcon />
418
- <span>{branch}</span>
419
- <Chevron />
420
- </button>
421
- {open ? (
422
- <div id={listId} role="menu" aria-label="Branches" className={`${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-menu-end`}>
423
- <MenuBody
424
- query={query}
425
- onQuery={setQuery}
426
- showFilter={(names?.length ?? 0) > 7}
427
- empty={shown?.length === 0}
428
- emptyLabel={query.trim() ? "Nothing matches" : "No branches"}
429
- error={error}
430
- loading={shown === null && !error}
431
- >
432
- {shown?.map((item) => (
433
- <button
434
- key={item.key}
435
- type="button"
436
- role="menuitem"
437
- className={`${DEVBAR_CLASS}-option${item.label === branch ? ` ${DEVBAR_CLASS}-current` : ""}`}
438
- aria-current={item.label === branch ? "true" : undefined}
439
- disabled={busy}
440
- onClick={() => {
441
- if (item.label === branch) {
442
- onToggle();
443
- return;
444
- }
445
- setBusy(true);
446
- setError(null);
447
- void switchPreviewBranch({
448
- workspaceId,
449
- branch: item.label,
450
- tree,
451
- redirect: location.pathname + location.search + location.hash,
452
- previewEntryPath,
453
- })
454
- .catch((err: unknown) => {
455
- setBusy(false);
456
- setError(err instanceof Error ? err.message : "Couldn't switch branch");
457
- });
458
- }}
459
- >
460
- <span>{item.label}</span>
461
- {item.label === defaultBranch ? <span className={`${DEVBAR_CLASS}-meta`}>default</span> : null}
462
- </button>
463
- ))}
464
- </MenuBody>
465
- </div>
466
- ) : null}
467
- </div>
1359
+ const shown = names ? filterItems(names.map((name) => ({ key: name, label: name })), query) : null;
1360
+ return createPortal(
1361
+ <div id={listId} role="menu" aria-label="Branches" className={`${DEVBAR_CLASS}-menu ${DEVBAR_CLASS}-page-menu`}>
1362
+ {workspaceId ? (
1363
+ <MenuBody
1364
+ query={query}
1365
+ onQuery={setQuery}
1366
+ showFilter={(names?.length ?? 0) > 7}
1367
+ empty={shown?.length === 0}
1368
+ emptyLabel={query.trim() ? "Nothing matches" : "No branches"}
1369
+ error={error}
1370
+ loading={shown === null && !error}
1371
+ >
1372
+ {shown?.map((item) => (
1373
+ <button
1374
+ key={item.key}
1375
+ type="button"
1376
+ role="menuitem"
1377
+ className={`${DEVBAR_CLASS}-option${item.label === branch ? ` ${DEVBAR_CLASS}-current` : ""}`}
1378
+ aria-current={item.label === branch ? "true" : undefined}
1379
+ disabled={busy}
1380
+ onClick={() => {
1381
+ if (item.label === branch) {
1382
+ onClose();
1383
+ return;
1384
+ }
1385
+ setBusy(true);
1386
+ setError(null);
1387
+ void switchPreviewBranch({
1388
+ workspaceId,
1389
+ branch: item.label,
1390
+ tree,
1391
+ redirect: location.pathname + location.search + location.hash,
1392
+ previewEntryPath,
1393
+ }).catch((err: unknown) => {
1394
+ setBusy(false);
1395
+ setError(err instanceof Error ? err.message : "Couldn't switch branch");
1396
+ });
1397
+ }}
1398
+ >
1399
+ <span>{item.label}</span>
1400
+ {item.label === defaultBranch ? <span className={`${DEVBAR_CLASS}-meta`}>default</span> : null}
1401
+ </button>
1402
+ ))}
1403
+ </MenuBody>
1404
+ ) : (
1405
+ <p className={`${DEVBAR_CLASS}-note`}>{branch}</p>
1406
+ )}
1407
+ </div>,
1408
+ bar,
468
1409
  );
469
1410
  }
470
1411
 
471
1412
  function MenuBody({
472
1413
  query,
473
1414
  onQuery,
1415
+ inputRef,
1416
+ searchClassName,
474
1417
  showFilter,
475
1418
  empty,
476
1419
  emptyLabel,
@@ -480,33 +1423,51 @@ function MenuBody({
480
1423
  }: {
481
1424
  query: string;
482
1425
  onQuery: (value: string) => void;
1426
+ inputRef?: Ref<HTMLInputElement>;
1427
+ searchClassName?: string;
483
1428
  showFilter: boolean;
484
1429
  empty: boolean;
485
1430
  emptyLabel: string;
486
1431
  error: string | null;
487
1432
  loading: boolean;
488
- children: ReactNode;
1433
+ children?: ReactNode;
489
1434
  }) {
490
1435
  return (
491
1436
  <>
492
1437
  {showFilter ? (
493
- <input
494
- value={query}
495
- onChange={(event) => onQuery(event.target.value)}
496
- placeholder="Filter"
497
- aria-label="Filter"
498
- autoComplete="off"
499
- spellCheck={false}
500
- />
1438
+ <div className={`${DEVBAR_CLASS}-page-menu-search`}>
1439
+ <input
1440
+ ref={inputRef}
1441
+ value={query}
1442
+ onChange={(event) => onQuery(event.target.value)}
1443
+ placeholder="Search files"
1444
+ aria-label="Search files"
1445
+ autoComplete="off"
1446
+ spellCheck={false}
1447
+ className={searchClassName}
1448
+ />
1449
+ </div>
1450
+ ) : null}
1451
+ {children ? (
1452
+ <>
1453
+ {error ? <p className={`${DEVBAR_CLASS}-note ${DEVBAR_CLASS}-note-error`}>{error}</p> : null}
1454
+ {loading ? <p className={`${DEVBAR_CLASS}-note`}>Loading…</p> : null}
1455
+ {empty && !error ? <p className={`${DEVBAR_CLASS}-note`}>{emptyLabel}</p> : null}
1456
+ {children}
1457
+ </>
501
1458
  ) : null}
502
- {error ? <p className={`${DEVBAR_CLASS}-note ${DEVBAR_CLASS}-note-error`}>{error}</p> : null}
503
- {loading ? <p className={`${DEVBAR_CLASS}-note`}>Loading…</p> : null}
504
- {empty && !error ? <p className={`${DEVBAR_CLASS}-note`}>{emptyLabel}</p> : null}
505
- {children}
506
1459
  </>
507
1460
  );
508
1461
  }
509
1462
 
1463
+ function Chevron() {
1464
+ return (
1465
+ <svg className={`${DEVBAR_CLASS}-chevron`} width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden>
1466
+ <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
1467
+ </svg>
1468
+ );
1469
+ }
1470
+
510
1471
  function filterItems<T extends { label: string }>(items: readonly T[], query: string): T[] {
511
1472
  const needle = query.trim().toLowerCase();
512
1473
  if (!needle) return [...items];
@@ -542,169 +1503,399 @@ export async function switchPreviewBranch(input: {
542
1503
  assign(`${input.previewEntryPath}${joiner}ticket=${encodeURIComponent(minted.ticket)}`);
543
1504
  }
544
1505
 
1506
+ const LIGHT_CSS = `
1507
+ .${DEVBAR_CLASS}-host {
1508
+ position: absolute;
1509
+ width: 0;
1510
+ height: 0;
1511
+ overflow: visible;
1512
+ border: 0;
1513
+ padding: 0;
1514
+ text-decoration: none;
1515
+ color: inherit;
1516
+ }
1517
+ html.backmatter-pointing, html.backmatter-pointing * { cursor: crosshair !important; }
1518
+ html.backmatter-pointing [data-backmatter-panel],
1519
+ html.backmatter-pointing [data-backmatter-panel] *,
1520
+ html.backmatter-pointing [data-backmatter-devbar] { cursor: pointer !important; }
1521
+ html.backmatter-blocks [data-backmatter-block] {
1522
+ outline: 1.5px solid oklch(0.56 0.21 295);
1523
+ outline-offset: 2px;
1524
+ }
1525
+ `;
1526
+
545
1527
  const BAR_CSS = `
546
- @keyframes backmatter-devbar-in {
547
- from { opacity: 0; transform: translateY(8px); }
548
- to { opacity: 1; transform: none; }
1528
+ .${DEVBAR_CLASS}-root {
1529
+ --preview: ${PREVIEW_CHROME_BORDER};
1530
+ --preview-hover: oklch(0.50 0.20 295);
1531
+ --preview-deep: oklch(0.48 0.20 295);
1532
+ --preview-line: oklch(0.66 0.16 295);
1533
+ --preview-outline: oklch(0.72 0.13 295);
1534
+ --preview-ink: oklch(0.98 0.01 295);
1535
+ --preview-ink-soft: oklch(0.88 0.05 295);
1536
+ --preview-on-white: oklch(0.36 0.16 295);
1537
+ --panel: #ffffff;
1538
+ --line: #e7e7e4;
1539
+ --line2: #d8d8d4;
1540
+ --fg: #17171a;
1541
+ --fg2: #5d5d62;
1542
+ --fg3: #8e8e93;
1543
+ --hover: #f3f3f1;
1544
+ --live: oklch(0.66 0.13 150);
1545
+ --shadow-live: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 28px -8px rgba(0, 0, 0, 0.18);
1546
+ --shadow-preview: 0 1px 2px rgba(0, 0, 0, 0.08), 0 10px 28px -8px oklch(0.45 0.2 295 / 0.5);
1547
+ --ease-out: ${DEVBAR_EASE_OUT};
1548
+ --ease-in: ${DEVBAR_EASE_IN};
1549
+ --ox: 28px;
1550
+ --oy: 20px;
1551
+ color-scheme: light;
1552
+ text-decoration: none;
549
1553
  }
550
1554
  .${DEVBAR_CLASS} {
551
- position: fixed;
552
- right: max(16px, env(safe-area-inset-right, 0px));
553
- bottom: 0;
1555
+ all: initial;
1556
+ position: var(--bm-devbar-position, fixed);
1557
+ right: var(--bm-devbar-right, max(16px, env(safe-area-inset-right, 0px)));
1558
+ left: var(--bm-devbar-left, auto);
1559
+ bottom: var(--bm-devbar-bottom, 0px);
554
1560
  z-index: 2147483000;
555
- isolation: isolate;
556
- display: flex;
557
- align-items: center;
558
- gap: 8px;
1561
+ display: block;
559
1562
  width: max-content;
560
1563
  max-width: calc(100vw - 32px);
561
- min-height: 40px;
562
- padding: 4px 6px calc(4px + env(safe-area-inset-bottom, 0px)) 4px;
1564
+ height: 40px;
563
1565
  box-sizing: border-box;
564
- border: 2px solid ${PREVIEW_CHROME_BORDER};
565
- border-bottom: 0;
566
- border-radius: 14px 14px 0 0;
567
- background: #ffffff;
568
- color: #71717a;
569
- box-shadow: 0 10px 28px rgba(24, 24, 27, 0.12), 0 1px 2px rgba(24, 24, 27, 0.06);
570
- font: 500 13px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
571
- -webkit-font-smoothing: antialiased;
1566
+ transform-origin: 100% 100%;
1567
+ pointer-events: auto;
1568
+ color: var(--fg);
1569
+ font-family: "Geist Sans", Geist, ui-sans-serif, system-ui, sans-serif;
1570
+ font-size: 12.5px;
1571
+ font-weight: 500;
1572
+ font-style: normal;
1573
+ line-height: 1;
1574
+ letter-spacing: normal;
572
1575
  font-variant-numeric: tabular-nums;
573
- animation: backmatter-devbar-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both;
1576
+ -webkit-font-smoothing: antialiased;
1577
+ text-align: left;
1578
+ cursor: default;
1579
+ user-select: none;
1580
+ transition: color 250ms ease;
574
1581
  }
575
- @media (prefers-reduced-motion: reduce) {
576
- .${DEVBAR_CLASS} { animation: none; }
1582
+ .${DEVBAR_CLASS}[data-hidden="true"] { pointer-events: none; }
1583
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS} { color: var(--preview-ink); }
1584
+ .${DEVBAR_CLASS} button, .${DEVBAR_CLASS} input {
1585
+ all: unset;
1586
+ box-sizing: border-box;
1587
+ font: inherit;
1588
+ color: inherit;
1589
+ cursor: pointer;
1590
+ text-decoration: none;
1591
+ -webkit-appearance: none;
1592
+ appearance: none;
577
1593
  }
578
- .${DEVBAR_CLASS}-mode {
579
- display: inline-flex;
580
- flex-shrink: 0;
1594
+ .${DEVBAR_CLASS} button {
1595
+ transition: transform 160ms var(--ease-out), background-color 150ms ease, color 250ms ease, border-color 300ms ease;
1596
+ }
1597
+ .${DEVBAR_CLASS} button:active:not(:disabled) { transform: scale(0.97); }
1598
+ .${DEVBAR_CLASS} button:disabled { cursor: default; }
1599
+ .${DEVBAR_CLASS} button:focus-visible, .${DEVBAR_CLASS}-menu input:focus-visible {
1600
+ outline: none;
1601
+ box-shadow: inset 0 0 0 1.5px currentColor;
1602
+ }
1603
+ .${DEVBAR_CLASS} input.${DEVBAR_CLASS}-page-search:focus,
1604
+ .${DEVBAR_CLASS} input.${DEVBAR_CLASS}-page-search:focus-visible {
1605
+ outline: none;
1606
+ box-shadow: none;
1607
+ }
1608
+ .${DEVBAR_CLASS}-clip {
1609
+ position: relative;
1610
+ height: 40px;
1611
+ overflow: hidden;
1612
+ box-sizing: border-box;
1613
+ border: 1px solid var(--line);
1614
+ border-bottom: 0;
1615
+ border-radius: 12px 12px 0 0;
1616
+ background: var(--panel);
1617
+ box-shadow: var(--shadow-live);
1618
+ transition: border-color 300ms ease;
1619
+ }
1620
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-clip {
1621
+ border-color: transparent;
1622
+ background: transparent;
1623
+ box-shadow: none;
1624
+ }
1625
+ .${DEVBAR_CLASS}-flood {
1626
+ position: absolute;
1627
+ z-index: 0;
1628
+ inset: -1px -1px 0 -1px;
1629
+ border-radius: 12px 12px 0 0;
1630
+ background: var(--preview);
1631
+ pointer-events: none;
1632
+ clip-path: circle(0px at var(--ox) var(--oy));
1633
+ transition: clip-path ${DEVBAR_FLOOD_EXIT_MS}ms var(--ease-in);
1634
+ }
1635
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-flood {
1636
+ clip-path: circle(150% at var(--ox) var(--oy));
1637
+ transition: clip-path ${DEVBAR_FLOOD_ENTER_MS}ms var(--ease-out);
1638
+ }
1639
+ .${DEVBAR_CLASS}-root[data-settled] .${DEVBAR_CLASS}-flood { transition: none; }
1640
+ .${DEVBAR_CLASS}-row {
1641
+ position: relative;
1642
+ z-index: 1;
1643
+ display: flex;
581
1644
  align-items: center;
582
1645
  gap: 2px;
583
- padding: 2px;
584
- border-radius: 9px;
585
- background: #f4f4f5;
1646
+ height: 39px;
1647
+ padding: 0 8px 0 12px;
1648
+ white-space: nowrap;
586
1649
  }
587
- .${DEVBAR_CLASS}-mode button {
1650
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-mode {
588
1651
  display: inline-flex;
1652
+ flex-shrink: 0;
589
1653
  align-items: center;
590
1654
  gap: 6px;
591
- height: 26px;
592
- padding: 0 8px;
593
- border: 0;
594
- border-radius: 7px;
595
- background: transparent;
596
- color: #71717a;
597
- font: inherit;
598
- cursor: pointer;
599
- }
600
- .${DEVBAR_CLASS}-mode button[aria-pressed="true"] {
601
- background: #f4efe4;
602
- color: #18181b;
1655
+ height: 28px;
1656
+ margin-right: 10px;
1657
+ padding: 0 10px;
1658
+ border: 1px solid transparent;
1659
+ border-radius: 8px;
1660
+ background: var(--preview);
1661
+ color: var(--preview-ink);
603
1662
  }
604
- .${DEVBAR_CLASS}-dot {
605
- width: 6px;
606
- height: 6px;
607
- border-radius: 999px;
608
- background: #c4841d;
609
- flex-shrink: 0;
1663
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-mode {
1664
+ background: transparent;
1665
+ border-color: var(--preview-outline);
610
1666
  }
611
- .${DEVBAR_CLASS}-pop { position: relative; min-width: 0; }
1667
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-mode:active:not(:disabled) { background: var(--preview-deep); }
1668
+ .${DEVBAR_CLASS}-mark { flex-shrink: 0; }
612
1669
  .${DEVBAR_CLASS}-crumb {
613
1670
  display: inline-flex;
614
1671
  align-items: center;
615
- gap: 6px;
616
1672
  min-width: 0;
617
- max-width: min(180px, 100%);
1673
+ max-width: min(180px, 28vw);
618
1674
  height: 28px;
619
- padding: 0 6px;
620
- border: 0;
621
- border-radius: 7px;
622
- background: transparent;
623
- color: #18181b;
624
- font: inherit;
1675
+ padding: 0 8px;
1676
+ border-radius: 8px;
1677
+ color: var(--fg2);
625
1678
  }
626
- button.${DEVBAR_CLASS}-crumb { cursor: pointer; }
627
- .${DEVBAR_CLASS}-crumb > span {
1679
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-crumb { color: var(--preview-ink-soft); }
1680
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-page {
1681
+ min-width: 0;
1682
+ max-width: min(200px, 34vw);
1683
+ }
1684
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-page > span {
628
1685
  min-width: 0;
629
1686
  overflow: hidden;
630
1687
  text-overflow: ellipsis;
631
1688
  white-space: nowrap;
632
1689
  }
633
- .${DEVBAR_CLASS}-branch { color: #52525b; }
634
- .${DEVBAR_CLASS}-chevron { flex-shrink: 0; color: #a1a1aa; }
1690
+ .${DEVBAR_CLASS}-page-on { background: var(--hover); }
1691
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-page-on {
1692
+ background: rgba(255, 255, 255, 0.18);
1693
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
1694
+ }
1695
+ .${DEVBAR_CLASS}-chevron { flex-shrink: 0; color: var(--fg3); }
1696
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-chevron { color: var(--preview-ink-soft); }
635
1697
  .${DEVBAR_CLASS}-rule {
636
1698
  width: 1px;
637
1699
  height: 16px;
638
1700
  flex-shrink: 0;
639
- background: #e4e4e7;
1701
+ margin: 0 6px;
1702
+ background: var(--line2);
640
1703
  }
641
- .${DEVBAR_CLASS}-actions {
1704
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-rule { background: var(--preview-line); }
1705
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-btn,
1706
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-publish {
642
1707
  display: inline-flex;
643
1708
  flex-shrink: 0;
644
1709
  align-items: center;
645
1710
  gap: 6px;
646
- }
647
- .${DEVBAR_CLASS}-btn {
648
- display: inline-flex;
649
- align-items: center;
650
- gap: 6px;
651
1711
  height: 28px;
652
- padding: 0 10px;
653
- border: 1px solid #e4e4e7;
1712
+ padding: 0 8px;
654
1713
  border-radius: 8px;
655
- background: #ffffff;
656
- color: #18181b;
657
- font: inherit;
658
- font-size: 13px;
659
- cursor: pointer;
1714
+ border: 0;
1715
+ text-decoration: none;
660
1716
  }
661
- .${DEVBAR_CLASS}-agent {
662
- border-color: transparent;
663
- background: #f4f4f5;
1717
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-btn span,
1718
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-publish span {
1719
+ text-decoration: none;
664
1720
  }
665
- .${DEVBAR_CLASS}-agent kbd {
666
- font-family: Geist Mono, ui-monospace, SFMono-Regular, Menlo, monospace;
1721
+ .${DEVBAR_CLASS}-kbd, .${DEVBAR_CLASS}-field, .${DEVBAR_CLASS}-when {
1722
+ font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
667
1723
  font-size: 11px;
668
- font-weight: 400;
669
- color: #71717a;
1724
+ font-weight: 500;
670
1725
  }
1726
+ .${DEVBAR_CLASS}-kbd, .${DEVBAR_CLASS}-when, .${DEVBAR_CLASS}-field { color: var(--fg3); }
1727
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-kbd,
1728
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-field,
1729
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-when { color: var(--preview-ink-soft); }
671
1730
  .${DEVBAR_CLASS}-count { font-variant-numeric: tabular-nums; }
672
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on,
673
- .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-on:hover:not(:disabled) {
674
- background: #18181b;
675
- border-color: #18181b;
676
- color: #fafafa;
1731
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-publish:not(:disabled) { background: #17171a; color: #fff; }
1732
+ .${DEVBAR_CLASS}-root[data-mode="live"] button.${DEVBAR_CLASS}-publish:disabled {
1733
+ background: #f0f0ee;
1734
+ color: var(--fg3);
677
1735
  }
678
- .${DEVBAR_CLASS}-on kbd { color: #d4d4d8; }
679
- .${DEVBAR_CLASS}-menu {
1736
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-publish:not(:disabled) {
1737
+ background: #fff;
1738
+ color: var(--preview-on-white);
1739
+ }
1740
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-publish:disabled {
1741
+ background: rgba(255, 255, 255, 0.14);
1742
+ color: var(--preview-ink-soft);
1743
+ }
1744
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-btn:disabled { opacity: 0.45; }
1745
+ .${DEVBAR_CLASS}-changes, .${DEVBAR_CLASS}-steps, .${DEVBAR_CLASS}-published, .${DEVBAR_CLASS}-step {
1746
+ display: inline-flex;
1747
+ align-items: center;
1748
+ gap: 6px;
1749
+ min-width: 0;
1750
+ }
1751
+ .${DEVBAR_CLASS}-diff { display: inline-flex; gap: 4px; }
1752
+ .${DEVBAR_CLASS}-add, .${DEVBAR_CLASS}-del { font-variant-numeric: tabular-nums; }
1753
+ .${DEVBAR_CLASS}-dash { color: var(--preview-ink-soft); }
1754
+ .${DEVBAR_CLASS}-spin {
1755
+ width: 12px;
1756
+ height: 12px;
1757
+ flex-shrink: 0;
1758
+ box-sizing: border-box;
1759
+ border: 1.5px solid var(--preview-line);
1760
+ border-top-color: #fff;
1761
+ border-radius: 999px;
1762
+ animation: ${DEVBAR_CLASS}-spin 800ms linear infinite;
1763
+ }
1764
+ .${DEVBAR_CLASS}-ring {
1765
+ width: 12px;
1766
+ height: 12px;
1767
+ flex-shrink: 0;
1768
+ box-sizing: border-box;
1769
+ border: 1.5px solid var(--preview-line);
1770
+ border-radius: 999px;
1771
+ }
1772
+ .${DEVBAR_CLASS}-done, .${DEVBAR_CLASS}-check { color: var(--live); }
1773
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-done { color: var(--preview-ink); }
1774
+ .${DEVBAR_CLASS}-shimmer {
1775
+ display: inline-block;
1776
+ background-image: linear-gradient(90deg, var(--preview-ink-soft) 0%, #fff 45%, var(--preview-ink-soft) 90%);
1777
+ background-size: 200% 100%;
1778
+ -webkit-background-clip: text;
1779
+ background-clip: text;
1780
+ color: transparent;
1781
+ animation: ${DEVBAR_CLASS}-shimmer 1.4s linear infinite;
1782
+ }
1783
+ .${DEVBAR_CLASS}-progress {
680
1784
  position: absolute;
681
- bottom: calc(100% + 8px);
682
1785
  left: 0;
1786
+ bottom: 0;
683
1787
  z-index: 2;
1788
+ height: 2px;
1789
+ background: #fff;
1790
+ pointer-events: none;
1791
+ transition: width 300ms linear;
1792
+ }
1793
+ .${DEVBAR_CLASS}-sr {
1794
+ position: absolute;
1795
+ width: 1px;
1796
+ height: 1px;
1797
+ padding: 0;
1798
+ margin: -1px;
1799
+ overflow: hidden;
1800
+ clip: rect(0 0 0 0);
1801
+ white-space: nowrap;
1802
+ border: 0;
1803
+ }
1804
+ .${DEVBAR_CLASS}-menu {
1805
+ position: absolute;
1806
+ bottom: calc(100% + 8px);
1807
+ left: var(--menu-x, 12px);
1808
+ z-index: 3;
684
1809
  display: flex;
685
1810
  flex-direction: column;
686
1811
  width: 240px;
687
1812
  max-height: 280px;
688
1813
  overflow: auto;
689
1814
  padding: 4px;
690
- border: 1px solid #e4e4e7;
1815
+ border: 1px solid var(--line);
691
1816
  border-radius: 10px;
692
- background: #ffffff;
1817
+ background: var(--panel);
1818
+ color: var(--fg);
1819
+ box-shadow: var(--shadow-live);
1820
+ box-sizing: border-box;
1821
+ }
1822
+ .${DEVBAR_CLASS}-page-menu {
1823
+ display: flex;
1824
+ flex-direction: column;
1825
+ width: min(360px, calc(100vw - 32px));
1826
+ max-height: min(420px, calc(100vh - 96px));
1827
+ padding: 0;
1828
+ border-radius: 16px;
1829
+ overflow: hidden;
1830
+ }
1831
+ .${DEVBAR_CLASS}-page-menu-search {
1832
+ flex-shrink: 0;
1833
+ padding: 8px 12px 0;
1834
+ border-bottom: 1px solid var(--line);
1835
+ }
1836
+ .${DEVBAR_CLASS}-page-menu-list {
1837
+ display: flex;
1838
+ flex-direction: column;
1839
+ flex: 1 1 auto;
1840
+ gap: 1px;
1841
+ min-height: 240px;
1842
+ max-height: 320px;
1843
+ overflow: auto;
1844
+ padding: 4px 6px 6px;
1845
+ box-sizing: border-box;
1846
+ }
1847
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-page-menu {
1848
+ border-color: var(--preview-line);
1849
+ background: #fff;
1850
+ color: var(--fg);
693
1851
  box-shadow: 0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06);
694
1852
  }
695
- .${DEVBAR_CLASS}-menu-end { left: auto; right: 0; }
696
1853
  .${DEVBAR_CLASS}-menu input {
697
1854
  width: 100%;
698
- box-sizing: border-box;
699
1855
  height: 32px;
700
1856
  margin: 2px 0 4px;
701
1857
  padding: 0 8px;
702
- border: 1px solid #e4e4e7;
1858
+ border: 1px solid var(--line);
703
1859
  border-radius: 7px;
704
- background: #ffffff;
705
- color: #18181b;
706
- font: inherit;
1860
+ background: #fff;
1861
+ color: var(--fg);
707
1862
  font-size: 16px;
1863
+ cursor: text;
1864
+ user-select: text;
1865
+ }
1866
+ .${DEVBAR_CLASS} input.${DEVBAR_CLASS}-page-search {
1867
+ display: block;
1868
+ width: 100%;
1869
+ height: 40px;
1870
+ margin: 0;
1871
+ padding: 0 4px 10px;
1872
+ border: 0;
1873
+ border-radius: 0;
1874
+ background: transparent;
1875
+ color: var(--fg);
1876
+ font-size: 13px;
1877
+ font-weight: 500;
1878
+ line-height: 1.3;
1879
+ cursor: text;
1880
+ user-select: text;
1881
+ box-sizing: border-box;
1882
+ }
1883
+ .${DEVBAR_CLASS} input.${DEVBAR_CLASS}-page-search::placeholder {
1884
+ color: var(--fg3);
1885
+ font-weight: 400;
1886
+ }
1887
+ .${DEVBAR_CLASS}-page-menu-foot {
1888
+ display: flex;
1889
+ flex-direction: column;
1890
+ flex-shrink: 0;
1891
+ padding: 0 8px 8px;
1892
+ }
1893
+ .${DEVBAR_CLASS}-page-menu-foot::before {
1894
+ content: "";
1895
+ display: block;
1896
+ height: 1px;
1897
+ margin: 0 4px 6px;
1898
+ background: var(--line);
708
1899
  }
709
1900
  .${DEVBAR_CLASS}-option {
710
1901
  display: flex;
@@ -713,14 +1904,17 @@ button.${DEVBAR_CLASS}-crumb { cursor: pointer; }
713
1904
  width: 100%;
714
1905
  min-height: 32px;
715
1906
  padding: 0 8px;
716
- border: 0;
717
1907
  border-radius: 7px;
718
- background: transparent;
719
- color: #18181b;
720
- font: inherit;
721
- font-size: 13px;
722
1908
  text-align: left;
723
- cursor: pointer;
1909
+ box-sizing: border-box;
1910
+ }
1911
+ .${DEVBAR_CLASS}-page-menu .${DEVBAR_CLASS}-option {
1912
+ flex-shrink: 0;
1913
+ min-height: 30px;
1914
+ padding: 5px 12px;
1915
+ border-radius: 7px;
1916
+ font-size: 12.5px;
1917
+ line-height: 1.25;
724
1918
  }
725
1919
  .${DEVBAR_CLASS}-option > span:first-child {
726
1920
  min-width: 0;
@@ -728,42 +1922,214 @@ button.${DEVBAR_CLASS}-crumb { cursor: pointer; }
728
1922
  text-overflow: ellipsis;
729
1923
  white-space: nowrap;
730
1924
  }
731
- .${DEVBAR_CLASS}-current { background: #f4f4f5; }
1925
+ .${DEVBAR_CLASS}-current { background: var(--hover); }
732
1926
  .${DEVBAR_CLASS}-meta {
733
1927
  margin-left: auto;
734
1928
  flex-shrink: 0;
735
- color: #71717a;
1929
+ color: var(--fg3);
736
1930
  font-size: 11px;
737
- font-weight: 400;
1931
+ font-weight: 500;
1932
+ }
1933
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-gear {
1934
+ display: inline-flex;
1935
+ align-items: center;
1936
+ justify-content: center;
1937
+ width: 28px;
1938
+ height: 28px;
1939
+ margin-left: 4px;
1940
+ border-radius: 8px;
1941
+ background: var(--hover);
1942
+ color: var(--fg2);
1943
+ }
1944
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-gear svg {
1945
+ display: block;
1946
+ flex-shrink: 0;
1947
+ }
1948
+ .${DEVBAR_CLASS}-gear-on { background: var(--line); color: var(--fg); }
1949
+ .${DEVBAR_CLASS}-publish-on { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); }
1950
+ .${DEVBAR_CLASS}-root[data-mode="live"] button.${DEVBAR_CLASS}-publish-on {
1951
+ box-shadow: inset 0 0 0 1px var(--line2);
1952
+ }
1953
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-gear {
1954
+ background: rgba(255, 255, 255, 0.14);
1955
+ color: var(--preview-ink);
1956
+ }
1957
+ .${DEVBAR_CLASS}-settings {
1958
+ right: var(--menu-right, 8px);
1959
+ left: auto;
1960
+ width: 328px;
1961
+ max-height: min(480px, calc(100vh - 80px));
1962
+ padding: 8px;
1963
+ border-radius: 16px;
1964
+ overflow: auto;
1965
+ box-sizing: border-box;
1966
+ }
1967
+ .${DEVBAR_CLASS}-settings-head {
1968
+ display: flex;
1969
+ align-items: center;
1970
+ justify-content: space-between;
1971
+ gap: 12px;
1972
+ min-height: 28px;
1973
+ padding: 4px 12px 8px;
1974
+ }
1975
+ .${DEVBAR_CLASS}-settings-kicker {
1976
+ color: var(--fg3);
1977
+ font-size: 11px;
1978
+ font-weight: 600;
1979
+ letter-spacing: 0.08em;
1980
+ text-transform: uppercase;
1981
+ }
1982
+ .${DEVBAR_CLASS}-settings-place {
1983
+ min-width: 0;
1984
+ overflow: hidden;
1985
+ color: var(--fg3);
1986
+ font-size: 12px;
1987
+ font-weight: 500;
1988
+ text-overflow: ellipsis;
1989
+ white-space: nowrap;
1990
+ }
1991
+ .${DEVBAR_CLASS}-settings-row,
1992
+ .${DEVBAR_CLASS}-settings-action,
1993
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-settings-action {
1994
+ display: flex;
1995
+ align-items: center;
1996
+ gap: 10px;
1997
+ width: 100%;
1998
+ min-height: 40px;
1999
+ padding: 0 12px;
2000
+ border-radius: 8px;
2001
+ color: var(--fg);
2002
+ text-align: left;
2003
+ box-sizing: border-box;
2004
+ }
2005
+ .${DEVBAR_CLASS}-settings-row.${DEVBAR_CLASS}-settings-theme {
2006
+ flex-wrap: wrap;
2007
+ row-gap: 8px;
2008
+ padding-bottom: 4px;
2009
+ }
2010
+ .${DEVBAR_CLASS}-settings-theme > span:nth-child(2) {
2011
+ flex: 1 1 auto;
2012
+ }
2013
+ .${DEVBAR_CLASS}-settings-row > span:nth-child(2),
2014
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-settings-action > span:nth-child(2) {
2015
+ min-width: 0;
2016
+ flex: 1;
2017
+ }
2018
+ .${DEVBAR_CLASS}-settings-icon { flex-shrink: 0; color: var(--fg2); }
2019
+ .${DEVBAR_CLASS}-settings-rule { height: 1px; margin: 6px 12px; background: var(--line); }
2020
+ .${DEVBAR_CLASS}-segment {
2021
+ display: inline-flex;
2022
+ flex-shrink: 0;
2023
+ padding: 2px;
2024
+ border-radius: 9px;
2025
+ background: var(--hover);
2026
+ }
2027
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-segment-btn {
2028
+ height: 26px;
2029
+ padding: 0 10px;
2030
+ border-radius: 7px;
2031
+ color: var(--fg2);
2032
+ font-size: 12.5px;
2033
+ }
2034
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-segment-on {
2035
+ background: #fff;
2036
+ color: var(--fg);
2037
+ font-weight: 600;
2038
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
2039
+ }
2040
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-switch {
2041
+ position: relative;
2042
+ flex-shrink: 0;
2043
+ width: 36px;
2044
+ height: 22px;
2045
+ margin-left: auto;
2046
+ border-radius: 999px;
2047
+ background: #e4e4e1;
2048
+ }
2049
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-switch-on { background: var(--preview); }
2050
+ .${DEVBAR_CLASS}-knob {
2051
+ position: absolute;
2052
+ top: 2px;
2053
+ left: 2px;
2054
+ width: 18px;
2055
+ height: 18px;
2056
+ border-radius: 999px;
2057
+ background: #fff;
2058
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.16);
2059
+ transition: transform 160ms var(--ease-out);
2060
+ }
2061
+ .${DEVBAR_CLASS}-switch-on .${DEVBAR_CLASS}-knob { transform: translateX(14px); }
2062
+ .${DEVBAR_CLASS}-settings .${DEVBAR_CLASS}-kbd { margin-left: auto; color: var(--fg3); }
2063
+ .${DEVBAR_CLASS}-settings-email {
2064
+ margin-left: auto;
2065
+ max-width: 46%;
2066
+ overflow: hidden;
2067
+ color: var(--fg3);
2068
+ font-size: 12px;
2069
+ font-weight: 500;
2070
+ text-align: right;
2071
+ text-overflow: ellipsis;
2072
+ white-space: nowrap;
2073
+ }
2074
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-settings-back {
2075
+ display: inline-flex;
2076
+ align-items: center;
2077
+ gap: 6px;
2078
+ min-height: 28px;
2079
+ color: var(--fg);
2080
+ font-weight: 600;
2081
+ }
2082
+ .${DEVBAR_CLASS}-settings-back svg { transform: rotate(90deg); }
2083
+ .${DEVBAR_CLASS}-settings-static { cursor: default; }
2084
+ .${DEVBAR_CLASS}-menu-split {
2085
+ display: flex;
2086
+ flex-direction: column;
2087
+ margin-top: 4px;
2088
+ padding-top: 4px;
2089
+ border-top: 1px solid var(--line);
738
2090
  }
739
2091
  .${DEVBAR_CLASS}-note {
740
2092
  margin: 0;
741
2093
  padding: 8px;
742
- color: #71717a;
2094
+ color: var(--fg2);
743
2095
  font-size: 12px;
744
- font-weight: 400;
2096
+ font-weight: 500;
2097
+ }
2098
+ .${DEVBAR_CLASS}-page-menu .${DEVBAR_CLASS}-note {
2099
+ padding: 8px 12px;
2100
+ font-size: 12.5px;
745
2101
  }
746
2102
  .${DEVBAR_CLASS}-note-error { color: #b91c1c; }
2103
+ .${DEVBAR_CLASS}-note-muted { color: var(--fg3); }
747
2104
  @media (hover: hover) {
748
- .${DEVBAR_CLASS}-mode button:hover:not(:disabled):not([aria-pressed="true"]) { color: #18181b; }
749
- .${DEVBAR_CLASS}-crumb:hover:not(:disabled) { background: #f4f4f5; }
750
- .${DEVBAR_CLASS}-btn:hover:not(:disabled):not(.${DEVBAR_CLASS}-on) { background: #fafafa; }
751
- .${DEVBAR_CLASS}-agent:hover:not(:disabled):not(.${DEVBAR_CLASS}-on) { background: #ececee; }
752
- .${DEVBAR_CLASS}-option:hover:not(:disabled) { background: #f4f4f5; }
753
- }
754
- .${DEVBAR_CLASS} button:focus-visible {
755
- outline: 2px solid #18181b;
756
- outline-offset: 2px;
2105
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-mode:hover:not(:disabled) { background: var(--preview-hover); }
2106
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-mode:hover:not(:disabled) { background: var(--preview-hover); }
2107
+ .${DEVBAR_CLASS}-root[data-mode="live"] button.${DEVBAR_CLASS}-btn:hover:not(:disabled),
2108
+ .${DEVBAR_CLASS}-root[data-mode="live"] button.${DEVBAR_CLASS}-page:hover:not(:disabled),
2109
+ .${DEVBAR_CLASS}-root[data-mode="live"] button.${DEVBAR_CLASS}-gear:hover,
2110
+ .${DEVBAR_CLASS}-option:hover:not(:disabled),
2111
+ .${DEVBAR_CLASS} button.${DEVBAR_CLASS}-settings-action:hover { background: var(--hover); }
2112
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-btn:hover:not(:disabled),
2113
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-page:hover:not(:disabled) { background: var(--preview-hover); }
2114
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-publish:hover:not(:disabled),
2115
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-publish:active:not(:disabled) { background: #fff; }
2116
+ .${DEVBAR_CLASS}-root[data-mode="preview"] button.${DEVBAR_CLASS}-btn:active:not(:disabled) { background: var(--preview-deep); }
757
2117
  }
758
- .${DEVBAR_CLASS} button:disabled { cursor: default; opacity: 0.5; }
759
- .${DEVBAR_CLASS}-menu input:focus-visible {
760
- outline: 2px solid #18181b;
761
- outline-offset: 1px;
2118
+ @media (prefers-reduced-motion: reduce) {
2119
+ .${DEVBAR_CLASS}-flood,
2120
+ .${DEVBAR_CLASS} button,
2121
+ .${DEVBAR_CLASS}-clip,
2122
+ .${DEVBAR_CLASS}-progress,
2123
+ .${DEVBAR_CLASS}-knob { transition: none !important; }
2124
+ .${DEVBAR_CLASS}-flood { clip-path: none; opacity: 0; }
2125
+ .${DEVBAR_CLASS}-root[data-mode="preview"] .${DEVBAR_CLASS}-flood { opacity: 1; }
762
2126
  }
763
- html.backmatter-pointing, html.backmatter-pointing * { cursor: crosshair !important; }
764
- html.backmatter-pointing .${DEVBAR_CLASS},
765
- html.backmatter-pointing .${DEVBAR_CLASS} *,
766
- html.backmatter-pointing [data-backmatter-panel],
767
- html.backmatter-pointing [data-backmatter-panel] * { cursor: pointer !important; }
768
- html.backmatter-pointing .${DEVBAR_CLASS} button:disabled { cursor: default !important; }
2127
+ @keyframes ${DEVBAR_CLASS}-spin { to { transform: rotate(360deg); } }
2128
+ @keyframes ${DEVBAR_CLASS}-shimmer {
2129
+ from { background-position: 200% 0; }
2130
+ to { background-position: -200% 0; }
2131
+ }
2132
+ :host(.${DEVBAR_CLASS}-pointing) .${DEVBAR_CLASS},
2133
+ :host(.${DEVBAR_CLASS}-pointing) .${DEVBAR_CLASS} * { cursor: pointer !important; }
2134
+ :host(.${DEVBAR_CLASS}-pointing) .${DEVBAR_CLASS} button:disabled { cursor: default !important; }
769
2135
  `;