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