@backmatter/studio 0.0.5 → 0.0.7

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 (48) hide show
  1. package/dist/chunks/{EmbedPublishPage-hx1xoAhI.js → EmbedPublishPage-88VsmOhi.js} +13 -13
  2. package/dist/chunks/{UiKitPage-BOunq6mr.js → UiKitPage-X4hkFL2d.js} +9 -9
  3. package/dist/chunks/{WorkspaceRoutes-CTFTWu3L.js → WorkspaceRoutes-DXzdWqN3.js} +20334 -18542
  4. package/dist/chunks/{dashboard-upmiawx5.js → dashboard-DCHRI0WD.js} +422 -400
  5. package/dist/chunks/{index-CBvevwfT.js → index-BS0-Bvbk.js} +2 -2
  6. package/dist/chunks/{status-bar-action-DiXhfcj3.js → status-bar-action-wj1KZ51m.js} +6925 -6567
  7. package/dist/dashboard.js +2 -2
  8. package/dist/index.js +18 -18
  9. package/dist/site-widget.js +788 -234
  10. package/dist/studio.css +454 -66
  11. package/package.json +3 -2
  12. package/src/App.tsx +2 -0
  13. package/src/WorkspaceRoutes.tsx +4 -0
  14. package/src/api.ts +104 -0
  15. package/src/chat/ChatMessage.tsx +10 -3
  16. package/src/chat/ChatPanel.tsx +1 -1
  17. package/src/chat/Composer.tsx +18 -18
  18. package/src/chat/chat-helpers.ts +58 -1
  19. package/src/chrome/AppSidebar.tsx +31 -156
  20. package/src/chrome/CommandPalette.tsx +211 -74
  21. package/src/chrome/ContentExplorerSidebar.tsx +1174 -0
  22. package/src/chrome/UserMenu.tsx +18 -3
  23. package/src/chrome/WorkspaceNavContext.tsx +1 -0
  24. package/src/chrome/command-palette-icons.tsx +152 -0
  25. package/src/chrome/content-path-tree.ts +95 -0
  26. package/src/chrome/content-repo-path.ts +52 -0
  27. package/src/chrome/explorer-icons.tsx +89 -0
  28. package/src/chrome/folder-create-context.ts +147 -0
  29. package/src/chrome/icons.tsx +13 -0
  30. package/src/pages/BlockSettingsDialog.tsx +4 -0
  31. package/src/pages/CliAuthorizePage.tsx +43 -0
  32. package/src/pages/DocEditorPage.tsx +33 -4
  33. package/src/pages/MediaFieldInput.tsx +139 -0
  34. package/src/pages/MediaGalleryPage.tsx +752 -0
  35. package/src/pages/MediaLightbox.tsx +460 -0
  36. package/src/pages/SettingsLayout.tsx +7 -1
  37. package/src/pages/SettingsUsagePage.tsx +187 -0
  38. package/src/pages/TypeDocsPage.tsx +2 -210
  39. package/src/pages/WorkspaceLayout.tsx +6 -1
  40. package/src/pages/media-grid.ts +190 -0
  41. package/src/pages/preview-url.ts +13 -2
  42. package/src/preview-frame.tsx +78 -0
  43. package/src/replica/useReplica.ts +9 -0
  44. package/src/site-bar.tsx +769 -0
  45. package/src/site-widget.tsx +33 -162
  46. package/src/ui/ConfirmDialog.tsx +1 -1
  47. package/src/workspace/permissions.ts +9 -0
  48. package/src/workspace-path.ts +13 -0
@@ -0,0 +1,769 @@
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";
5
+ import {
6
+ checkoutBranch,
7
+ createPreviewTicket,
8
+ fetchDocs,
9
+ fetchGit,
10
+ fetchSchema,
11
+ type DocListItem,
12
+ } from "./api";
13
+ import { GitBranchIcon } from "./chrome/icons";
14
+ import { matchAppHotkey, modifierSymbol } from "./chrome/hotkeys";
15
+ import { PublishIcon, SparkIcon } from "./chat/icons";
16
+ import { PREVIEW_CHROME_BORDER } from "./preview-frame";
17
+
18
+ export const DEVBAR_CLASS = "backmatter-devbar";
19
+
20
+ const DEFAULT_PREVIEW_ENTRY = "/api/backmatter/preview";
21
+
22
+ export type PageMenuDoc = {
23
+ locale: string;
24
+ type: string;
25
+ slug: string;
26
+ };
27
+
28
+ export type PageMenuItem = {
29
+ key: string;
30
+ label: string;
31
+ href: string;
32
+ current: boolean;
33
+ };
34
+
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;
65
+ }
66
+
67
+ type MenuState = "page" | "branch" | null;
68
+
69
+ export function SiteBar({
70
+ label,
71
+ branch,
72
+ tree,
73
+ doc,
74
+ exiting,
75
+ chatOpen,
76
+ publishOpen,
77
+ changeCount,
78
+ previewEntryPath = DEFAULT_PREVIEW_ENTRY,
79
+ siteMode = "preview",
80
+ onExit,
81
+ onEnterPreview,
82
+ onToggleChat,
83
+ onTogglePublish,
84
+ }: {
85
+ label: string;
86
+ branch?: string;
87
+ tree?: string;
88
+ doc: (PageMenuDoc & { workspaceId: string }) | null;
89
+ siteMode?: "preview" | "live";
90
+ exiting?: boolean;
91
+ chatOpen: boolean;
92
+ publishOpen: boolean;
93
+ changeCount: number;
94
+ previewEntryPath?: string;
95
+ onExit: () => void;
96
+ onEnterPreview?: () => void;
97
+ onToggleChat: () => void;
98
+ onTogglePublish: () => void;
99
+ }) {
100
+ const rootRef = useRef<HTMLElement>(null);
101
+ const [menu, setMenu] = useState<MenuState>(null);
102
+
103
+ useEffect(() => {
104
+ if (!menu) return;
105
+ 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;
113
+ setMenu(null);
114
+ }
115
+ document.addEventListener("pointerdown", onPointer);
116
+ document.addEventListener("keydown", onKey);
117
+ return () => {
118
+ document.removeEventListener("pointerdown", onPointer);
119
+ document.removeEventListener("keydown", onKey);
120
+ };
121
+ }, [menu]);
122
+
123
+ useEffect(() => {
124
+ function onKey(event: KeyboardEvent) {
125
+ if (matchAppHotkey(event) !== "chat") return;
126
+ setMenu(null);
127
+ }
128
+ document.addEventListener("keydown", onKey, true);
129
+ return () => document.removeEventListener("keydown", onKey, true);
130
+ }, []);
131
+
132
+ function dismissPanels() {
133
+ if (chatOpen) onToggleChat();
134
+ if (publishOpen) onTogglePublish();
135
+ }
136
+
137
+ function toggleMenu(next: Exclude<MenuState, null>) {
138
+ setMenu((open) => {
139
+ if (open === next) return null;
140
+ dismissPanels();
141
+ return next;
142
+ });
143
+ }
144
+
145
+ const publishEmphasis = publishOpen || changeCount > 0;
146
+
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>
178
+ <PageMenu
179
+ label={label}
180
+ doc={doc}
181
+ open={menu === "page"}
182
+ onToggle={() => toggleMenu("page")}
183
+ />
184
+ {branch ? (
185
+ <BranchMenu
186
+ branch={branch}
187
+ tree={tree}
188
+ workspaceId={doc?.workspaceId ?? null}
189
+ previewEntryPath={previewEntryPath}
190
+ open={menu === "branch"}
191
+ onToggle={() => toggleMenu("branch")}
192
+ />
193
+ ) : 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>
211
+ </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}
227
+ </span>
228
+ </button>
229
+ </div>
230
+ </footer>
231
+ </>
232
+ );
233
+ }
234
+
235
+ function Dot() {
236
+ return <span className={`${DEVBAR_CLASS}-dot`} aria-hidden />;
237
+ }
238
+
239
+ function Chevron() {
240
+ 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" />
243
+ </svg>
244
+ );
245
+ }
246
+
247
+ function PageMenu({
248
+ label,
249
+ doc,
250
+ open,
251
+ onToggle,
252
+ }: {
253
+ label: string;
254
+ doc: (PageMenuDoc & { workspaceId: string }) | null;
255
+ open: boolean;
256
+ onToggle: () => void;
257
+ }) {
258
+ const [docs, setDocs] = useState<DocListItem[] | null>(null);
259
+ const [models, setModels] = useState<RouteModel[] | null>(null);
260
+ const [error, setError] = useState<string | null>(null);
261
+ const [query, setQuery] = useState("");
262
+ const listId = useId();
263
+
264
+ useEffect(() => {
265
+ if (!open) setQuery("");
266
+ }, [open]);
267
+
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
+ );
282
+ })
283
+ .catch(() => {
284
+ if (!cancelled) setError("Couldn't load pages");
285
+ });
286
+ return () => {
287
+ cancelled = true;
288
+ };
289
+ }, [open, doc, docs]);
290
+
291
+ const items =
292
+ docs && models ? pageMenuItems({ docs, models, current: doc }) : null;
293
+ const shown = items ? filterItems(items, query) : null;
294
+
295
+ if (!doc) {
296
+ return <span className={`${DEVBAR_CLASS}-crumb`}>{label}</span>;
297
+ }
298
+
299
+ return (
300
+ <div className={`${DEVBAR_CLASS}-pop`}>
301
+ <button
302
+ type="button"
303
+ className={`${DEVBAR_CLASS}-crumb ${DEVBAR_CLASS}-page`}
304
+ aria-haspopup="menu"
305
+ aria-expanded={open}
306
+ aria-controls={listId}
307
+ onClick={onToggle}
308
+ >
309
+ <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>
345
+ );
346
+ }
347
+
348
+ function BranchMenu({
349
+ branch,
350
+ tree,
351
+ workspaceId,
352
+ previewEntryPath,
353
+ open,
354
+ onToggle,
355
+ }: {
356
+ branch: string;
357
+ tree?: string;
358
+ workspaceId: string | null;
359
+ previewEntryPath: string;
360
+ open: boolean;
361
+ onToggle: () => void;
362
+ }) {
363
+ const [names, setNames] = useState<string[] | null>(null);
364
+ const [defaultBranch, setDefaultBranch] = useState<string | null>(null);
365
+ const [error, setError] = useState<string | null>(null);
366
+ const [busy, setBusy] = useState(false);
367
+ const [query, setQuery] = useState("");
368
+ const listId = useId();
369
+
370
+ useEffect(() => {
371
+ if (!open) setQuery("");
372
+ }, [open]);
373
+
374
+ useEffect(() => {
375
+ if (!open || !workspaceId || names) return;
376
+ let cancelled = false;
377
+ void fetchGit(workspaceId)
378
+ .then((git) => {
379
+ if (cancelled) return;
380
+ const listed = git.branches.map((item) => item.name);
381
+ if (!listed.includes(branch)) listed.unshift(branch);
382
+ setNames(listed);
383
+ setDefaultBranch(git.defaultBranch);
384
+ })
385
+ .catch(() => {
386
+ if (!cancelled) setError("Couldn't load branches");
387
+ });
388
+ return () => {
389
+ cancelled = true;
390
+ };
391
+ }, [open, workspaceId, names, branch]);
392
+
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
+ }
403
+
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>
468
+ );
469
+ }
470
+
471
+ function MenuBody({
472
+ query,
473
+ onQuery,
474
+ showFilter,
475
+ empty,
476
+ emptyLabel,
477
+ error,
478
+ loading,
479
+ children,
480
+ }: {
481
+ query: string;
482
+ onQuery: (value: string) => void;
483
+ showFilter: boolean;
484
+ empty: boolean;
485
+ emptyLabel: string;
486
+ error: string | null;
487
+ loading: boolean;
488
+ children: ReactNode;
489
+ }) {
490
+ return (
491
+ <>
492
+ {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
+ />
501
+ ) : 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
+ </>
507
+ );
508
+ }
509
+
510
+ function filterItems<T extends { label: string }>(items: readonly T[], query: string): T[] {
511
+ const needle = query.trim().toLowerCase();
512
+ if (!needle) return [...items];
513
+ return items.filter((item) => item.label.toLowerCase().includes(needle));
514
+ }
515
+
516
+ export async function switchPreviewBranch(input: {
517
+ workspaceId: string;
518
+ branch: string;
519
+ tree?: string;
520
+ redirect: string;
521
+ previewEntryPath: string;
522
+ checkout?: typeof checkoutBranch;
523
+ ticket?: typeof createPreviewTicket;
524
+ assign?: (url: string) => void;
525
+ }): Promise<void> {
526
+ const checkout = input.checkout ?? checkoutBranch;
527
+ const ticket = input.ticket ?? createPreviewTicket;
528
+ const assign = input.assign ?? ((url: string) => location.assign(url));
529
+ await checkout(input.workspaceId, input.branch);
530
+ let minted: { ticket: string };
531
+ try {
532
+ minted = await ticket(input.workspaceId, {
533
+ redirect: input.redirect,
534
+ branch: input.branch,
535
+ tree: input.tree,
536
+ });
537
+ } catch (err) {
538
+ const message = err instanceof Error ? err.message : "Couldn't open the branch";
539
+ throw new Error(`Checked out ${input.branch}, but the preview didn't update. ${message}`);
540
+ }
541
+ const joiner = input.previewEntryPath.includes("?") ? "&" : "?";
542
+ assign(`${input.previewEntryPath}${joiner}ticket=${encodeURIComponent(minted.ticket)}`);
543
+ }
544
+
545
+ const BAR_CSS = `
546
+ @keyframes backmatter-devbar-in {
547
+ from { opacity: 0; transform: translateY(8px); }
548
+ to { opacity: 1; transform: none; }
549
+ }
550
+ .${DEVBAR_CLASS} {
551
+ position: fixed;
552
+ right: max(16px, env(safe-area-inset-right, 0px));
553
+ bottom: 0;
554
+ z-index: 2147483000;
555
+ isolation: isolate;
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 8px;
559
+ width: max-content;
560
+ max-width: calc(100vw - 32px);
561
+ min-height: 40px;
562
+ padding: 4px 6px calc(4px + env(safe-area-inset-bottom, 0px)) 4px;
563
+ 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;
572
+ font-variant-numeric: tabular-nums;
573
+ animation: backmatter-devbar-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both;
574
+ }
575
+ @media (prefers-reduced-motion: reduce) {
576
+ .${DEVBAR_CLASS} { animation: none; }
577
+ }
578
+ .${DEVBAR_CLASS}-mode {
579
+ display: inline-flex;
580
+ flex-shrink: 0;
581
+ align-items: center;
582
+ gap: 2px;
583
+ padding: 2px;
584
+ border-radius: 9px;
585
+ background: #f4f4f5;
586
+ }
587
+ .${DEVBAR_CLASS}-mode button {
588
+ display: inline-flex;
589
+ align-items: center;
590
+ 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;
603
+ }
604
+ .${DEVBAR_CLASS}-dot {
605
+ width: 6px;
606
+ height: 6px;
607
+ border-radius: 999px;
608
+ background: #c4841d;
609
+ flex-shrink: 0;
610
+ }
611
+ .${DEVBAR_CLASS}-pop { position: relative; min-width: 0; }
612
+ .${DEVBAR_CLASS}-crumb {
613
+ display: inline-flex;
614
+ align-items: center;
615
+ gap: 6px;
616
+ min-width: 0;
617
+ max-width: min(180px, 100%);
618
+ height: 28px;
619
+ padding: 0 6px;
620
+ border: 0;
621
+ border-radius: 7px;
622
+ background: transparent;
623
+ color: #18181b;
624
+ font: inherit;
625
+ }
626
+ button.${DEVBAR_CLASS}-crumb { cursor: pointer; }
627
+ .${DEVBAR_CLASS}-crumb > span {
628
+ min-width: 0;
629
+ overflow: hidden;
630
+ text-overflow: ellipsis;
631
+ white-space: nowrap;
632
+ }
633
+ .${DEVBAR_CLASS}-branch { color: #52525b; }
634
+ .${DEVBAR_CLASS}-chevron { flex-shrink: 0; color: #a1a1aa; }
635
+ .${DEVBAR_CLASS}-rule {
636
+ width: 1px;
637
+ height: 16px;
638
+ flex-shrink: 0;
639
+ background: #e4e4e7;
640
+ }
641
+ .${DEVBAR_CLASS}-actions {
642
+ display: inline-flex;
643
+ flex-shrink: 0;
644
+ align-items: center;
645
+ gap: 6px;
646
+ }
647
+ .${DEVBAR_CLASS}-btn {
648
+ display: inline-flex;
649
+ align-items: center;
650
+ gap: 6px;
651
+ height: 28px;
652
+ padding: 0 10px;
653
+ border: 1px solid #e4e4e7;
654
+ border-radius: 8px;
655
+ background: #ffffff;
656
+ color: #18181b;
657
+ font: inherit;
658
+ font-size: 13px;
659
+ cursor: pointer;
660
+ }
661
+ .${DEVBAR_CLASS}-agent {
662
+ border-color: transparent;
663
+ background: #f4f4f5;
664
+ }
665
+ .${DEVBAR_CLASS}-agent kbd {
666
+ font-family: Geist Mono, ui-monospace, SFMono-Regular, Menlo, monospace;
667
+ font-size: 11px;
668
+ font-weight: 400;
669
+ color: #71717a;
670
+ }
671
+ .${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;
677
+ }
678
+ .${DEVBAR_CLASS}-on kbd { color: #d4d4d8; }
679
+ .${DEVBAR_CLASS}-menu {
680
+ position: absolute;
681
+ bottom: calc(100% + 8px);
682
+ left: 0;
683
+ z-index: 2;
684
+ display: flex;
685
+ flex-direction: column;
686
+ width: 240px;
687
+ max-height: 280px;
688
+ overflow: auto;
689
+ padding: 4px;
690
+ border: 1px solid #e4e4e7;
691
+ border-radius: 10px;
692
+ background: #ffffff;
693
+ box-shadow: 0 16px 40px rgba(24, 24, 27, 0.14), 0 2px 8px rgba(24, 24, 27, 0.06);
694
+ }
695
+ .${DEVBAR_CLASS}-menu-end { left: auto; right: 0; }
696
+ .${DEVBAR_CLASS}-menu input {
697
+ width: 100%;
698
+ box-sizing: border-box;
699
+ height: 32px;
700
+ margin: 2px 0 4px;
701
+ padding: 0 8px;
702
+ border: 1px solid #e4e4e7;
703
+ border-radius: 7px;
704
+ background: #ffffff;
705
+ color: #18181b;
706
+ font: inherit;
707
+ font-size: 16px;
708
+ }
709
+ .${DEVBAR_CLASS}-option {
710
+ display: flex;
711
+ align-items: center;
712
+ gap: 8px;
713
+ width: 100%;
714
+ min-height: 32px;
715
+ padding: 0 8px;
716
+ border: 0;
717
+ border-radius: 7px;
718
+ background: transparent;
719
+ color: #18181b;
720
+ font: inherit;
721
+ font-size: 13px;
722
+ text-align: left;
723
+ cursor: pointer;
724
+ }
725
+ .${DEVBAR_CLASS}-option > span:first-child {
726
+ min-width: 0;
727
+ overflow: hidden;
728
+ text-overflow: ellipsis;
729
+ white-space: nowrap;
730
+ }
731
+ .${DEVBAR_CLASS}-current { background: #f4f4f5; }
732
+ .${DEVBAR_CLASS}-meta {
733
+ margin-left: auto;
734
+ flex-shrink: 0;
735
+ color: #71717a;
736
+ font-size: 11px;
737
+ font-weight: 400;
738
+ }
739
+ .${DEVBAR_CLASS}-note {
740
+ margin: 0;
741
+ padding: 8px;
742
+ color: #71717a;
743
+ font-size: 12px;
744
+ font-weight: 400;
745
+ }
746
+ .${DEVBAR_CLASS}-note-error { color: #b91c1c; }
747
+ @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;
757
+ }
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;
762
+ }
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; }
769
+ `;