@backmatter/studio 0.0.16 → 0.0.18

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 (45) hide show
  1. package/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-DkuQBiQZ.js} +2 -2
  2. package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DVvb3N-V.js} +27489 -26622
  3. package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-B4FITg6c.js} +2 -2
  4. package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
  5. package/dist/chunks/{index-D_BHF_8q.js → index-CegtXnEI.js} +1 -1
  6. package/dist/dashboard.js +2 -2
  7. package/dist/define.js +1 -1
  8. package/dist/index.js +4 -4
  9. package/dist/site-widget.css +45 -1
  10. package/dist/site-widget.js +2799 -2791
  11. package/dist/studio.css +113 -15
  12. package/package.json +2 -2
  13. package/src/api.ts +22 -0
  14. package/src/chat/ChatProvider.tsx +6 -0
  15. package/src/chat/Composer.tsx +3 -1
  16. package/src/chrome/Toast.tsx +115 -0
  17. package/src/chrome/WorkspaceStatusBar.tsx +12 -3
  18. package/src/chrome/commit-error.ts +13 -0
  19. package/src/chrome/pull-failure.ts +61 -0
  20. package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
  21. package/src/fields/FieldControl.tsx +771 -0
  22. package/src/fields/LinkFieldInput.tsx +315 -0
  23. package/src/fields/choose-field-input.ts +69 -0
  24. package/src/fields/link-targets.ts +60 -0
  25. package/src/fields/load-relation-documents.ts +35 -0
  26. package/src/fields/omit-field.ts +12 -0
  27. package/src/fields/relation-targets.ts +44 -0
  28. package/src/fields/resolve-object-fields.ts +21 -0
  29. package/src/host-path.ts +13 -0
  30. package/src/pages/BlockSettingsDialog.tsx +19 -115
  31. package/src/pages/BlocksList.tsx +43 -35
  32. package/src/pages/BodyProse.tsx +16 -1
  33. package/src/pages/CodeSessionPage.tsx +186 -248
  34. package/src/pages/DocEditorPage.tsx +168 -213
  35. package/src/pages/MediaFieldInput.tsx +609 -26
  36. package/src/pages/MediaGalleryPage.tsx +14 -3
  37. package/src/pages/PreviewAddress.tsx +263 -0
  38. package/src/pages/RenderedBody.tsx +17 -2
  39. package/src/pages/branch-notice.ts +16 -0
  40. package/src/pages/code-session-boot.ts +0 -12
  41. package/src/pages/code-session-context.tsx +2 -0
  42. package/src/pages/media-selection.ts +79 -0
  43. package/src/shell-resets.css +15 -0
  44. package/src/ui/Input.tsx +178 -20
  45. package/src/ui/popup.ts +11 -2
@@ -1,27 +1,23 @@
1
1
  // packages/studio/src/pages/CodeSessionPage.tsx
2
- import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import type { RouteModel } from "@backmatter/core";
4
4
  import { useOutletContext } from "react-router";
5
5
  import * as stylex from "@stylexjs/stylex";
6
- import { Combobox } from "@base-ui/react/combobox";
7
6
  import {
8
- IconArrowRotateClockwise,
9
- IconArrowUpRight,
10
7
  IconCheckmark1Small,
11
8
  IconCrossSmall,
12
9
  IconEyeOpen,
13
10
  IconFiles,
14
- IconGlobe,
15
11
  } from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
16
12
  import { ChatPanel } from "../chat/ChatPanel";
17
- import { ChatProvider, ChatSessionScope } from "../chat/ChatProvider";
13
+ import { ChatProvider, ChatSessionScope, useChatBusy } from "../chat/ChatProvider";
18
14
  import { useReplicaState } from "../replica/useReplica";
15
+ import { routesForContentFiles } from "../content-routes";
19
16
  import { colors, radius, space, type } from "../styles/tokens.stylex";
20
17
  import { apiCredentials, apiHeaders, apiUrl } from "../studio-config";
21
18
  import { Text } from "../ui/Text";
22
- import { cx, popup, popupClassName } from "../ui/popup";
23
19
  import type { WorkspaceOutletContext } from "../workspace/types";
24
- import { bootSteps, hostPath, type BootStep, type HostPhase } from "./code-session-boot";
20
+ import { bootSteps, type BootStep, type HostPhase } from "./code-session-boot";
25
21
  import {
26
22
  frameAddress,
27
23
  framedLocation,
@@ -30,12 +26,13 @@ import {
30
26
  previewUrl,
31
27
  type HostPreview,
32
28
  } from "./code-session-frame";
33
- import { contentRoutes, matchingRoutes } from "./code-session-routes";
29
+ import { PreviewAddress } from "./PreviewAddress";
34
30
  import { CodeSessionFiles, type FilesView } from "./CodeSessionFiles";
35
31
  import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
36
32
  import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
37
33
  import type { WorkspaceFile } from "./code-session-files";
38
- import { fetchCodeSessionEdits } from "../api";
34
+ import { clearCodeSessionEdits, fetchCodeSessionEdits, updateCodeSessionBranch } from "../api";
35
+ import { branchNoticeKind, type BranchNoticeKind } from "./branch-notice";
39
36
  import { usePublishCodeSession } from "./code-session-context";
40
37
  import { sessionDocument } from "./session-preview";
41
38
  import { useSessionPreview } from "./use-session-preview";
@@ -80,6 +77,10 @@ export function CodeSessionScreen({
80
77
  models = [],
81
78
  files,
82
79
  chat,
80
+ notice = null,
81
+ noticeBusy = false,
82
+ onUpdateBranch = () => {},
83
+ onClearEdits = () => {},
83
84
  }: {
84
85
  state: OpenState;
85
86
  host: HostView;
@@ -108,9 +109,16 @@ export function CodeSessionScreen({
108
109
  /** Models whose routes name the document under a pointed block. */
109
110
  models?: readonly RouteModel[];
110
111
  chat: React.ReactNode;
112
+ notice?: BranchNoticeKind | null;
113
+ /** Chat is writing, or the checkout is already being replaced. */
114
+ noticeBusy?: boolean;
115
+ onUpdateBranch?: () => void;
116
+ onClearEdits?: () => void;
111
117
  }) {
112
118
  // The entry's ticket sets the host's cookie on first load; later loads ride that cookie.
113
119
  // A reload uses the route the frame is on, which can differ from the last address typed.
120
+ const chatBusy = useChatBusy();
121
+ const actionBusy = noticeBusy || chatBusy;
114
122
  const frameSrc = preview ? frameAddress(preview, reloads > 0 ? location : path, reloads) : null;
115
123
  const frameRef = useRef<HTMLIFrameElement>(null);
116
124
  const heldFrameRef = useRef<HTMLIFrameElement | null>(null);
@@ -182,7 +190,7 @@ export function CodeSessionScreen({
182
190
  </TabButton>
183
191
  </div>
184
192
 
185
- <AddressBar
193
+ <PreviewAddress
186
194
  path={location}
187
195
  enabled={frameSrc !== null && tab === "preview"}
188
196
  routes={routes}
@@ -201,6 +209,38 @@ export function CodeSessionScreen({
201
209
  </div>
202
210
  </div>
203
211
 
212
+ {notice ? (
213
+ <div {...stylex.props(styles.notice)} role="status">
214
+ <span {...stylex.props(styles.noticeText)}>
215
+ {notice === "edits"
216
+ ? "This branch has new commits. Commit them, or clear the edit set, before updating."
217
+ : notice === "fold"
218
+ ? "This branch has new commits. The last commit is still folding."
219
+ : "This branch has new commits."}
220
+ </span>
221
+ {notice === "update" ? (
222
+ <button
223
+ type="button"
224
+ {...stylex.props(styles.noticeButton)}
225
+ disabled={actionBusy}
226
+ onClick={onUpdateBranch}
227
+ >
228
+ Update branch
229
+ </button>
230
+ ) : null}
231
+ {notice === "edits" ? (
232
+ <button
233
+ type="button"
234
+ {...stylex.props(styles.noticeButton)}
235
+ disabled={actionBusy}
236
+ onClick={onClearEdits}
237
+ >
238
+ Clear edit set
239
+ </button>
240
+ ) : null}
241
+ </div>
242
+ ) : null}
243
+
204
244
  <div {...stylex.props(styles.canvas)}>
205
245
  {tab === "files" ? (
206
246
  <CodeSessionFiles {...files} />
@@ -280,170 +320,6 @@ function TabButton({
280
320
  );
281
321
  }
282
322
 
283
- function AddressBar({
284
- path,
285
- enabled,
286
- routes,
287
- routesPending,
288
- onOpenRoutes,
289
- onPath,
290
- onReload,
291
- onOpen,
292
- }: {
293
- path: string;
294
- enabled: boolean;
295
- routes: readonly string[];
296
- routesPending: boolean;
297
- onOpenRoutes?: () => void;
298
- onPath: (path: string) => void;
299
- onReload: () => void;
300
- onOpen: () => void;
301
- }) {
302
- const [query, setQuery] = useState<string | null>(null);
303
- const [open, setOpen] = useState(false);
304
- const picked = useRef(false);
305
- const fieldRef = useRef<HTMLButtonElement>(null);
306
- const inputRef = useRef<HTMLInputElement>(null);
307
- const shown = query ? matchingRoutes(routes, query) : [...routes];
308
-
309
- useEffect(() => {
310
- if (!open) return;
311
- const input = inputRef.current;
312
- input?.focus();
313
- input?.select();
314
- }, [open]);
315
-
316
- return (
317
- <div {...stylex.props(styles.address)}>
318
- <button
319
- type="button"
320
- aria-label="Reload preview"
321
- disabled={!enabled}
322
- onClick={onReload}
323
- {...stylex.props(styles.iconButton)}
324
- >
325
- <IconArrowRotateClockwise size={14} ariaHidden />
326
- </button>
327
- <Combobox.Root
328
- items={shown}
329
- filteredItems={shown}
330
- inputValue={query ?? ""}
331
- disabled={!enabled}
332
- open={open}
333
- value={routes.includes(path) ? path : null}
334
- onOpenChange={(next) => {
335
- if (!enabled && next) return;
336
- setOpen(next);
337
- if (next) onOpenRoutes?.();
338
- else setQuery(null);
339
- }}
340
- onInputValueChange={(value, details) => {
341
- if (details.reason === "input-change" || details.reason === "input-clear") {
342
- setQuery(value);
343
- }
344
- }}
345
- onValueChange={(value) => {
346
- if (typeof value !== "string") return;
347
- picked.current = true;
348
- setQuery(null);
349
- onPath(hostPath(value));
350
- queueMicrotask(() => {
351
- picked.current = false;
352
- });
353
- }}
354
- >
355
- <Combobox.Trigger
356
- ref={fieldRef}
357
- aria-label="Preview path"
358
- disabled={!enabled}
359
- data-no-ring
360
- className={cx(styles.addressTrigger)}
361
- >
362
- <IconGlobe size={13} ariaHidden />
363
- <span {...stylex.props(styles.addressPath)}>{path}</span>
364
- </Combobox.Trigger>
365
- <Combobox.Portal>
366
- <Combobox.Positioner
367
- anchor={fieldRef}
368
- side="bottom"
369
- align="start"
370
- sideOffset={6}
371
- className={cx(popup.positioner)}
372
- >
373
- <Combobox.Popup
374
- aria-label="Pages"
375
- className={(state) => popupClassName(state, styles.routePopup)}
376
- >
377
- <form
378
- onSubmit={(event: FormEvent<HTMLFormElement>) => {
379
- event.preventDefault();
380
- const fromList = picked.current;
381
- picked.current = false;
382
- if (fromList) return;
383
- const typed = event.currentTarget.querySelector("input")?.value;
384
- if (typed?.trim()) onPath(hostPath(typed));
385
- setOpen(false);
386
- setQuery(null);
387
- }}
388
- >
389
- <Combobox.Input
390
- ref={inputRef}
391
- placeholder="Find a page"
392
- aria-label="Find a page"
393
- spellCheck={false}
394
- autoComplete="off"
395
- data-no-ring
396
- className={cx(popup.quietInput, styles.routeSearch)}
397
- onKeyDown={(event) => {
398
- if (event.key !== "Enter") return;
399
- // A highlighted row is chosen by the combobox. A bare Enter goes to the typed path.
400
- if (event.currentTarget.getAttribute("aria-activedescendant")) return;
401
- const typed = event.currentTarget.value.trim();
402
- if (!typed) return;
403
- picked.current = true;
404
- onPath(hostPath(typed));
405
- }}
406
- />
407
- </form>
408
- <div {...stylex.props(popup.separator)} />
409
- <Combobox.Empty className={cx(popup.empty)}>
410
- {routesPending ? "Loading pages" : "No pages"}
411
- </Combobox.Empty>
412
- <Combobox.List className={cx(styles.routeList)}>
413
- {(route: string) => (
414
- <Combobox.Item
415
- key={route}
416
- value={route}
417
- className={(state) =>
418
- cx(
419
- popup.item,
420
- styles.routeItem,
421
- state.highlighted && popup.itemHighlighted,
422
- state.selected && popup.itemSelected,
423
- )
424
- }
425
- >
426
- {route}
427
- </Combobox.Item>
428
- )}
429
- </Combobox.List>
430
- </Combobox.Popup>
431
- </Combobox.Positioner>
432
- </Combobox.Portal>
433
- </Combobox.Root>
434
- <button
435
- type="button"
436
- aria-label="Open preview in a new tab"
437
- disabled={!enabled}
438
- onClick={onOpen}
439
- {...stylex.props(styles.iconButton)}
440
- >
441
- <IconArrowUpRight size={14} ariaHidden />
442
- </button>
443
- </div>
444
- );
445
- }
446
-
447
323
  function BootCard({
448
324
  title,
449
325
  steps,
@@ -556,16 +432,26 @@ export default function CodeSessionPage() {
556
432
 
557
433
  const sessionId = state.kind === "ready" ? state.sessionId : null;
558
434
  const [editCount, setEditCount] = useState(0);
435
+ const [foldPending, setFoldPending] = useState(false);
436
+ const [baseSha, setBaseSha] = useState<string | null>(null);
437
+ const [updating, setUpdating] = useState(false);
559
438
  const publishCodeSession = usePublishCodeSession();
560
439
 
561
440
  const refreshEdits = useCallback(() => {
562
441
  if (!sessionId) {
563
442
  setEditCount(0);
443
+ setFoldPending(false);
564
444
  return;
565
445
  }
566
446
  void fetchCodeSessionEdits(workspace.id, sessionId)
567
- .then((list) => setEditCount(list.files.length))
568
- .catch(() => setEditCount(0));
447
+ .then((list) => {
448
+ setEditCount(list.files.length);
449
+ setFoldPending(list.foldPending);
450
+ })
451
+ .catch(() => {
452
+ setEditCount(0);
453
+ setFoldPending(false);
454
+ });
569
455
  }, [sessionId, workspace.id]);
570
456
 
571
457
  // The host's dev server does not push a reload through the preview proxy.
@@ -591,9 +477,9 @@ export default function CodeSessionPage() {
591
477
  publishCodeSession(null);
592
478
  return;
593
479
  }
594
- publishCodeSession({ sessionId, editCount, refresh: refreshEdits });
480
+ publishCodeSession({ sessionId, editCount, refresh: refreshEdits, updating });
595
481
  return () => publishCodeSession(null);
596
- }, [sessionId, editCount, refreshEdits, publishCodeSession]);
482
+ }, [sessionId, editCount, refreshEdits, publishCodeSession, updating]);
597
483
 
598
484
  // The agent and the dev server change the checkout, so read it fresh when the list is opened.
599
485
  const loadCheckout = useCallback(async () => {
@@ -666,6 +552,7 @@ export default function CodeSessionPage() {
666
552
  useEffect(() => {
667
553
  let cancelled = false;
668
554
  setState({ kind: "loading" });
555
+ setBaseSha(null);
669
556
  setHost({ phase: null, furthest: null, log: "", timedOut: false });
670
557
  setPreview(null);
671
558
  const stored = storedPreviewPath(workspace.id);
@@ -707,8 +594,9 @@ export default function CodeSessionPage() {
707
594
  setState({ kind: "fail", error: text || `request failed: ${res.status}` });
708
595
  return;
709
596
  }
710
- const body = (await res.json()) as { session?: { id?: string } };
597
+ const body = (await res.json()) as { session?: { id?: string; baseSha?: string } };
711
598
  const sessionId = body.session?.id;
599
+ setBaseSha(typeof body.session?.baseSha === "string" ? body.session.baseSha : null);
712
600
  if (!sessionId) {
713
601
  setState({ kind: "fail", error: "invalid response" });
714
602
  return;
@@ -752,6 +640,78 @@ export default function CodeSessionPage() {
752
640
  };
753
641
  }, [workspace.id, branch]);
754
642
 
643
+ async function bootAgain(id: string, openPath: string) {
644
+ setPath(openPath);
645
+ setLocation(openPath);
646
+ setReloads(0);
647
+ setPreview(null);
648
+ setHost({ phase: null, furthest: null, log: "", timedOut: false });
649
+ const statusUrl = apiUrl(`/workspaces/${workspace.id}/code-sessions/${id}/status`);
650
+ const deadline = Date.now() + HOST_WAIT_MS;
651
+ while (Date.now() < deadline) {
652
+ const polled = await fetch(statusUrl, {
653
+ credentials: apiCredentials(),
654
+ headers: apiHeaders(),
655
+ }).catch(() => null);
656
+ if (polled?.ok) {
657
+ const status = (await polled.json()) as { phase: HostPhase; log: string };
658
+ setHost((current) => ({
659
+ phase: status.phase,
660
+ furthest:
661
+ status.phase === "installing" || status.phase === "starting"
662
+ ? status.phase
663
+ : current.furthest,
664
+ log: status.log,
665
+ timedOut: false,
666
+ }));
667
+ if (status.phase === "ready") {
668
+ const entry = await requestPreview(workspace.id, id);
669
+ if (entry) setPreview(entry);
670
+ else setState({ kind: "fail", error: "The preview could not be opened." });
671
+ return;
672
+ }
673
+ if (status.phase === "failed" || status.phase === "stopped") return;
674
+ }
675
+ await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS));
676
+ }
677
+ setHost((current) => ({ ...current, timedOut: true }));
678
+ }
679
+
680
+ async function onUpdateBranch() {
681
+ if (!sessionId || updating) return;
682
+ setUpdating(true);
683
+ try {
684
+ const result = await updateCodeSessionBranch(workspace.id, sessionId);
685
+ setBaseSha(result.baseSha);
686
+ await bootAgain(sessionId, location);
687
+ } catch {
688
+ // The checkout stayed on its commit, and the preview that is up stays up.
689
+ } finally {
690
+ setUpdating(false);
691
+ refreshEdits();
692
+ }
693
+ }
694
+
695
+ function onClearEdits() {
696
+ if (!sessionId || updating) return;
697
+ const confirmed = window.confirm(
698
+ "Clear the edit set? The files go back to this session's commit. The chat stays.",
699
+ );
700
+ if (!confirmed) return;
701
+ setUpdating(true);
702
+ void (async () => {
703
+ try {
704
+ await clearCodeSessionEdits(workspace.id, sessionId);
705
+ await bootAgain(sessionId, location);
706
+ } catch {
707
+ // The edit set is still there.
708
+ } finally {
709
+ setUpdating(false);
710
+ refreshEdits();
711
+ }
712
+ })();
713
+ }
714
+
755
715
  async function openInNewTab() {
756
716
  if (state.kind !== "ready") return;
757
717
  // The iframe's cookie is partitioned under Studio, so a new tab needs its own ticket.
@@ -792,7 +752,7 @@ export default function CodeSessionPage() {
792
752
  live={live}
793
753
  onReload={() => navigate(location)}
794
754
  onOpen={() => void openInNewTab()}
795
- routes={contentRoutes(models, fileList ?? [])}
755
+ routes={routesForContentFiles(models, fileList ?? [])}
796
756
  routesPending={routesPending}
797
757
  onOpenRoutes={openRoutes}
798
758
  files={{
@@ -803,6 +763,19 @@ export default function CodeSessionPage() {
803
763
  onSelect: (path) => void selectFile(path),
804
764
  }}
805
765
  chat={chatColumn}
766
+ notice={
767
+ updating
768
+ ? null
769
+ : branchNoticeKind({
770
+ baseSha,
771
+ headSha: git?.head?.sha ?? null,
772
+ editCount,
773
+ foldPending,
774
+ })
775
+ }
776
+ noticeBusy={updating}
777
+ onUpdateBranch={() => void onUpdateBranch()}
778
+ onClearEdits={onClearEdits}
806
779
  previewSize={previewSize}
807
780
  onPreviewSize={(size) => {
808
781
  setPreviewSize(size);
@@ -940,6 +913,38 @@ const styles = stylex.create({
940
913
  backgroundColor: colors.text,
941
914
  opacity: { default: 1, ":disabled": 0.3 },
942
915
  },
916
+ notice: {
917
+ display: "flex",
918
+ alignItems: "center",
919
+ justifyContent: "space-between",
920
+ gap: space.md,
921
+ paddingInline: space.md,
922
+ paddingBlock: space.sm,
923
+ borderBottomWidth: 1,
924
+ borderBottomStyle: "solid",
925
+ borderBottomColor: colors.border,
926
+ backgroundColor: colors.bgSurface,
927
+ },
928
+ noticeText: {
929
+ fontSize: type.sm,
930
+ color: colors.text,
931
+ },
932
+ noticeButton: {
933
+ flexShrink: 0,
934
+ font: "inherit",
935
+ fontSize: type.xs,
936
+ fontWeight: 500,
937
+ cursor: { default: "pointer", ":disabled": "default" },
938
+ paddingBlock: "4px",
939
+ paddingInline: space.sm,
940
+ borderWidth: 1,
941
+ borderStyle: "solid",
942
+ borderColor: colors.border,
943
+ borderRadius: radius.sm,
944
+ color: colors.text,
945
+ backgroundColor: colors.bgPanelInset,
946
+ opacity: { default: 1, ":disabled": 0.4 },
947
+ },
943
948
  stage: {
944
949
  display: "flex",
945
950
  flex: 1,
@@ -988,76 +993,9 @@ const styles = stylex.create({
988
993
  backgroundColor: colors.bgSurface,
989
994
  boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
990
995
  },
991
- address: {
992
- display: "flex",
993
- alignItems: "center",
994
- gap: space.xs,
995
- minWidth: 0,
996
- },
997
996
  sizeSwitch: {
998
997
  justifySelf: "end",
999
998
  },
1000
- addressTrigger: {
1001
- display: "flex",
1002
- flex: 1,
1003
- alignItems: "center",
1004
- gap: space.xs,
1005
- minWidth: 0,
1006
- height: "28px",
1007
- paddingInline: space.sm,
1008
- borderWidth: 0,
1009
- borderRadius: "999px",
1010
- font: "inherit",
1011
- textAlign: "left",
1012
- cursor: { default: "pointer", ":disabled": "default" },
1013
- color: { default: colors.textMuted, ":disabled": colors.textMuted },
1014
- backgroundColor: colors.bgPanelInset,
1015
- },
1016
- addressPath: {
1017
- flex: 1,
1018
- minWidth: 0,
1019
- overflow: "hidden",
1020
- textOverflow: "ellipsis",
1021
- whiteSpace: "nowrap",
1022
- fontFamily: type.mono,
1023
- fontSize: type.xs,
1024
- color: colors.text,
1025
- },
1026
- routeSearch: {
1027
- fontFamily: type.mono,
1028
- fontSize: type.xs,
1029
- },
1030
- routePopup: {
1031
- width: "var(--anchor-width)",
1032
- minWidth: "220px",
1033
- maxHeight: "min(320px, var(--available-height))",
1034
- display: "flex",
1035
- flexDirection: "column",
1036
- overflowY: "hidden",
1037
- },
1038
- routeList: {
1039
- minHeight: 0,
1040
- overflowY: "auto",
1041
- },
1042
- routeItem: {
1043
- fontFamily: type.mono,
1044
- },
1045
- iconButton: {
1046
- display: "inline-flex",
1047
- alignItems: "center",
1048
- justifyContent: "center",
1049
- flexShrink: 0,
1050
- width: "28px",
1051
- height: "28px",
1052
- borderWidth: 0,
1053
- borderRadius: radius.sm,
1054
- cursor: { default: "pointer", ":disabled": "default" },
1055
- color: { default: colors.textMuted, ":disabled": colors.border },
1056
- backgroundColor: {
1057
- default: "transparent",
1058
- [hover]: { default: null, ":hover:not(:disabled)": colors.bgHover },
1059
- },
1060
- },
1061
999
  canvas: {
1062
1000
  position: "relative",
1063
1001
  display: "flex",