@backmatter/studio 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/chunks/UiKitApp-b8qFsmA-.js +2401 -0
  2. package/dist/chunks/WorkspaceRoutes-BKKEkAq7.js +90126 -0
  3. package/dist/chunks/{dashboard-DCHRI0WD.js → dashboard-BNUcbYU0.js} +380 -382
  4. package/dist/chunks/{index-BS0-Bvbk.js → index-laWAz5_p.js} +111 -112
  5. package/dist/dashboard.js +2 -2
  6. package/dist/index.js +287 -146
  7. package/dist/site-widget.css +1615 -0
  8. package/dist/site-widget.js +30896 -630
  9. package/dist/studio.css +303 -98
  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/AppShell.tsx +47 -10
  24. package/src/chrome/AppSidebar.tsx +9 -34
  25. package/src/chrome/ContentExplorerSidebar.tsx +149 -96
  26. package/src/chrome/UserMenu.tsx +3 -1
  27. package/src/chrome/content-tree-visibility.ts +55 -0
  28. package/src/chrome/explorer-icons.tsx +2 -4
  29. package/src/devbar-state.ts +125 -0
  30. package/src/index.css +3 -131
  31. package/src/pages/CliAuthorizePage.tsx +61 -28
  32. package/src/pages/MediaGalleryPage.tsx +144 -85
  33. package/src/pages/MediaLightbox.tsx +195 -96
  34. package/src/pages/SelectRepositoriesLink.tsx +28 -4
  35. package/src/pages/SettingsGeneralPage.tsx +25 -28
  36. package/src/pages/SettingsGithubPage.tsx +413 -124
  37. package/src/pages/SettingsUsagePage.tsx +23 -1
  38. package/src/pages/media-grid.ts +14 -1
  39. package/src/pages/ui-kit/UiKitApp.tsx +164 -0
  40. package/src/pages/ui-kit/buttons.tsx +30 -0
  41. package/src/pages/ui-kit/catalog.tsx +41 -0
  42. package/src/pages/ui-kit/colors.tsx +95 -0
  43. package/src/pages/ui-kit/devbar.tsx +160 -0
  44. package/src/pages/ui-kit/feedback.tsx +39 -0
  45. package/src/pages/ui-kit/fields.tsx +47 -0
  46. package/src/pages/ui-kit/lists.tsx +19 -0
  47. package/src/pages/ui-kit/overlays.tsx +39 -0
  48. package/src/pages/ui-kit/path.ts +9 -0
  49. package/src/pages/ui-kit/sections.tsx +32 -0
  50. package/src/pages/ui-kit/surfaces.tsx +121 -0
  51. package/src/pages/ui-kit/topic.tsx +112 -0
  52. package/src/pages/ui-kit/topics.ts +71 -0
  53. package/src/pages/ui-kit/type.tsx +55 -0
  54. package/src/preview-frame-radius.ts +51 -0
  55. package/src/preview-frame.tsx +93 -49
  56. package/src/shell-resets.css +155 -0
  57. package/src/site-bar.tsx +1791 -425
  58. package/src/site-dashboard-href.ts +21 -0
  59. package/src/site-devbar-overlay-inject.ts +15 -0
  60. package/src/site-devbar-overlay.css +217 -0
  61. package/src/site-pointer.ts +1 -1
  62. package/src/site-publish-menu.tsx +33 -0
  63. package/src/site-widget-styles.ts +7 -0
  64. package/src/site-widget.tsx +61 -21
  65. package/src/studio-prose-ui.css +39 -0
  66. package/src/theme/ThemeProvider.tsx +4 -1
  67. package/src/workspace-path.ts +18 -1
  68. package/dist/chunks/EmbedPublishPage-88VsmOhi.js +0 -149
  69. package/dist/chunks/UiKitPage-X4hkFL2d.js +0 -173
  70. package/dist/chunks/WorkspaceRoutes-DXzdWqN3.js +0 -59523
  71. package/dist/chunks/status-bar-action-wj1KZ51m.js +0 -30612
  72. package/src/pages/UiKitPage.tsx +0 -331
@@ -1,5 +1,5 @@
1
1
  // packages/studio/src/chrome/AppShell.tsx
2
- import { useEffect } from "react";
2
+ import { useEffect, useState } from "react";
3
3
  import { useLocation, useSearchParams } from "react-router";
4
4
  import * as stylex from "@stylexjs/stylex";
5
5
  import { ChatBar } from "../chat/ChatBar";
@@ -9,12 +9,13 @@ import { colors } from "../styles/tokens.stylex";
9
9
  import { TooltipProvider } from "../ui/Tooltip";
10
10
  import { cmsWorkspaceSlug, isOrgPickerPath } from "../workspace-path";
11
11
  import { AppSidebar } from "./AppSidebar";
12
+ import { WorkspaceRail } from "./ContentExplorerSidebar";
12
13
  import { CommandPalette } from "./CommandPalette";
13
14
  import { FloatingUserMenu } from "./UserMenu";
14
15
  import { EditorActionsProvider } from "./EditorActionsContext";
15
16
  import { EditorFocusProvider } from "./EditorFocusContext";
16
17
  import { EditorStatusProvider } from "./EditorStatusContext";
17
- import { WorkspaceNavProvider } from "./WorkspaceNavContext";
18
+ import { WorkspaceNavProvider, useWorkspaceNav } from "./WorkspaceNavContext";
18
19
  import { installParentChatHotkey } from "./hotkeys";
19
20
  import { WorkspaceStatusBar } from "./WorkspaceStatusBar";
20
21
 
@@ -40,28 +41,43 @@ function AppShellInner({ children }: { children: React.ReactNode }) {
40
41
  // Mount from the URL, not from nav. Waiting on nav remounts this tree when
41
42
  // WorkspaceLayout publishes (and later clears) nav, which loops loading.
42
43
  const workspaceKey = cmsWorkspaceSlug(location.pathname);
44
+ const workspaceNav = useWorkspaceNav();
43
45
  const orgPicker = isOrgPickerPath(location.pathname);
44
46
  const embed = searchParams.get("embed") === "1";
47
+ const [treeHost, setTreeHost] = useState<HTMLDivElement | null>(null);
48
+ const inWorkspace = Boolean(workspaceKey && !embed && !orgPicker);
45
49
 
46
50
  useEffect(() => {
47
51
  if (!embed) return;
48
52
  return installParentChatHotkey();
49
53
  }, [embed]);
50
54
 
55
+ const chat =
56
+ workspaceKey && !embed ? (
57
+ <ChatSessionScope>
58
+ <ChatBar />
59
+ <ChatPanel />
60
+ </ChatSessionScope>
61
+ ) : null;
62
+
51
63
  const shell = (
52
64
  <div {...stylex.props(styles.shell)}>
53
65
  {workspaceKey && !embed ? <WorkspaceStatusBar workspaceId={workspaceKey} /> : null}
54
66
  <div {...stylex.props(styles.body)}>
55
- {orgPicker || embed ? null : <AppSidebar />}
56
- <main {...stylex.props(styles.main)}>{children}</main>
67
+ {orgPicker || embed ? null : inWorkspace ? (
68
+ <WorkspaceRail workspace={workspaceNav} treeHost={treeHost} />
69
+ ) : (
70
+ <AppSidebar />
71
+ )}
72
+ <div {...stylex.props(styles.stage)}>
73
+ <div {...stylex.props(styles.stageBody)}>
74
+ <div ref={setTreeHost} {...stylex.props(styles.treeHost)} />
75
+ <main {...stylex.props(styles.main)}>{children}</main>
76
+ </div>
77
+ {chat}
78
+ </div>
57
79
  </div>
58
80
  {orgPicker ? <FloatingUserMenu /> : null}
59
- {workspaceKey && !embed ? (
60
- <ChatSessionScope>
61
- <ChatBar />
62
- <ChatPanel />
63
- </ChatSessionScope>
64
- ) : null}
65
81
  {embed ? null : <CommandPalette />}
66
82
  </div>
67
83
  );
@@ -89,6 +105,27 @@ const styles = stylex.create({
89
105
  flex: 1,
90
106
  overflow: "hidden",
91
107
  },
108
+ stage: {
109
+ display: "flex",
110
+ minWidth: 0,
111
+ minHeight: 0,
112
+ flex: 1,
113
+ flexDirection: "column",
114
+ overflow: "hidden",
115
+ },
116
+ stageBody: {
117
+ display: "flex",
118
+ minWidth: 0,
119
+ minHeight: 0,
120
+ flex: 1,
121
+ overflow: "hidden",
122
+ },
123
+ treeHost: {
124
+ display: "flex",
125
+ minWidth: 0,
126
+ minHeight: 0,
127
+ flexShrink: 0,
128
+ },
92
129
  main: {
93
130
  display: "flex",
94
131
  minWidth: 0,
@@ -1,30 +1,18 @@
1
1
  // packages/studio/src/chrome/AppSidebar.tsx
2
- import { useLocation } from "react-router";
3
2
  import * as stylex from "@stylexjs/stylex";
4
3
  import { useReplicaState } from "../replica/useReplica";
5
4
  import { colors, space, type } from "../styles/tokens.stylex";
6
- import { useWorkspaceNav } from "./WorkspaceNavContext";
7
5
  import { NavItem } from "./NavItem";
8
6
  import { UserMenu } from "./UserMenu";
9
- import { cmsWorkspaceSlug, isWorkspaceMediaPath, workspaceHomePath } from "../workspace-path";
7
+ import { workspaceHomePath } from "../workspace-path";
10
8
  import { DocIcon } from "./icons";
11
- import { ContentExplorerSidebar } from "./ContentExplorerSidebar";
12
- export function AppSidebar() {
13
- const location = useLocation();
14
- const workspaceNav = useWorkspaceNav();
15
- const inWorkspace = Boolean(cmsWorkspaceSlug(location.pathname));
16
- const railOnly = inWorkspace && isWorkspaceMediaPath(location.pathname);
17
9
 
10
+ /** Workspace list. Inside a workspace the explorer frame owns the rail. */
11
+ export function AppSidebar() {
18
12
  return (
19
- <aside {...stylex.props(styles.aside, railOnly && styles.asideRail)}>
20
- {inWorkspace && workspaceNav ? (
21
- <div {...stylex.props(styles.workspaceNav)}>
22
- <ContentExplorerSidebar workspace={workspaceNav} />
23
- </div>
24
- ) : (
25
- <WorkspaceListNav />
26
- )}
27
- <SidebarFooter compact={railOnly} />
13
+ <aside {...stylex.props(styles.aside)}>
14
+ <WorkspaceListNav />
15
+ <SidebarFooter />
28
16
  </aside>
29
17
  );
30
18
  }
@@ -68,21 +56,15 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
68
56
  return <p {...stylex.props(styles.section)}>{children}</p>;
69
57
  }
70
58
 
71
- function SidebarFooter({ compact = false }: { compact?: boolean }) {
59
+ function SidebarFooter() {
72
60
  return (
73
- <div {...stylex.props(styles.footer, compact && styles.footerCompact)}>
74
- <UserMenu compact={compact} />
61
+ <div {...stylex.props(styles.footer)}>
62
+ <UserMenu />
75
63
  </div>
76
64
  );
77
65
  }
78
66
 
79
67
  const styles = stylex.create({
80
- workspaceNav: {
81
- display: "flex",
82
- minHeight: 0,
83
- flex: 1,
84
- flexDirection: "column",
85
- },
86
68
  aside: {
87
69
  display: "flex",
88
70
  height: "100%",
@@ -95,10 +77,6 @@ const styles = stylex.create({
95
77
  borderRightColor: colors.border,
96
78
  backgroundColor: colors.bgSurface,
97
79
  },
98
- asideRail: {
99
- width: "48px",
100
- minWidth: "48px",
101
- },
102
80
  brand: {
103
81
  flexShrink: 0,
104
82
  borderBottomWidth: 1,
@@ -174,7 +152,4 @@ const styles = stylex.create({
174
152
  borderTopColor: colors.border,
175
153
  padding: space.md,
176
154
  },
177
- footerCompact: {
178
- padding: space.sm,
179
- },
180
155
  });
@@ -1,5 +1,6 @@
1
1
  // packages/studio/src/chrome/ContentExplorerSidebar.tsx
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
3
4
  import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
4
5
  import * as stylex from "@stylexjs/stylex";
5
6
  import { notifyTreeChanged, saveDoc, type DocListItem, type DocStatus } from "../api";
@@ -8,14 +9,15 @@ import { colors, radius, space, type } from "../styles/tokens.stylex";
8
9
  import { Tooltip } from "../ui/Tooltip";
9
10
  import {
10
11
  contentDocPath,
11
- contentTypePath,
12
+ contentRailHref,
12
13
  isWorkspaceContentPath,
14
+ isWorkspaceEditorPath,
13
15
  isWorkspaceMediaPath,
14
16
  isWorkspaceSettingsPath,
15
17
  mediaPath,
16
18
  settingsPath,
17
- workspaceHomePath,
18
19
  } from "../workspace-path";
20
+ import { UserMenu } from "./UserMenu";
19
21
  import {
20
22
  groupContentTypes,
21
23
  type ContentTypeNav,
@@ -53,6 +55,11 @@ import {
53
55
  type FolderCreateContext,
54
56
  } from "./folder-create-context";
55
57
  import { canWriteContent } from "../workspace/permissions";
58
+ import {
59
+ contentTreeVisible,
60
+ toggleContentTree,
61
+ useContentTreeOpen,
62
+ } from "./content-tree-visibility";
56
63
 
57
64
  const hover = "@media (hover: hover)";
58
65
  const statusDot = "#ea580c";
@@ -62,7 +69,36 @@ const TREE_PAD_PX = 8;
62
69
  const INDENT_PX = 8;
63
70
  const TWISTIE_PX = 16;
64
71
 
65
- export function ContentExplorerSidebar({ workspace }: { workspace: WorkspaceNav }) {
72
+ export function WorkspaceRail({
73
+ workspace,
74
+ treeHost,
75
+ }: {
76
+ workspace: WorkspaceNav | null;
77
+ treeHost: HTMLElement | null;
78
+ }) {
79
+ if (!workspace) {
80
+ return (
81
+ <RailColumn>
82
+ <div {...stylex.props(styles.rail, styles.railAccountOnly)}>
83
+ <UserMenu compact />
84
+ </div>
85
+ </RailColumn>
86
+ );
87
+ }
88
+ return <ContentExplorerSidebar workspace={workspace} treeHost={treeHost} />;
89
+ }
90
+
91
+ function RailColumn({ children }: { children?: React.ReactNode }) {
92
+ return <aside {...stylex.props(styles.railColumn)}>{children}</aside>;
93
+ }
94
+
95
+ function ContentExplorerSidebar({
96
+ workspace,
97
+ treeHost,
98
+ }: {
99
+ workspace: WorkspaceNav;
100
+ treeHost: HTMLElement | null;
101
+ }) {
66
102
  const location = useLocation();
67
103
  const [searchParams] = useSearchParams();
68
104
  const replica = useReplicaState();
@@ -128,80 +164,85 @@ export function ContentExplorerSidebar({ workspace }: { workspace: WorkspaceNav
128
164
 
129
165
  const empty = workspace.contentTypes.length === 0;
130
166
  const settingsActive = isWorkspaceSettingsPath(location.pathname);
131
- const showTree = !isWorkspaceMediaPath(location.pathname);
167
+ const treeOpen = useContentTreeOpen();
168
+ const showTree = contentTreeVisible(location.pathname, treeOpen);
132
169
  const canWrite = canWriteContent(workspace.role);
133
170
 
171
+ const tree = showTree ? (
172
+ <div {...stylex.props(styles.panel)}>
173
+ <header {...stylex.props(styles.panelHeader)}>
174
+ <span {...stylex.props(styles.panelTitle)}>Content</span>
175
+ <div {...stylex.props(styles.panelActions)}>
176
+ <IconButton label="New file (soon)" disabled>
177
+ <ExplorerHeaderNewFileIcon />
178
+ </IconButton>
179
+ <IconButton label="View options (soon)" disabled>
180
+ <ExplorerHeaderListIcon />
181
+ </IconButton>
182
+ </div>
183
+ </header>
184
+ <div {...stylex.props(styles.treeScroll)}>
185
+ {empty ? (
186
+ <p {...stylex.props(styles.emptyHint)}>
187
+ No content types yet. Pull from GitHub to load models.
188
+ </p>
189
+ ) : (
190
+ <>
191
+ {grouped.pages.length > 0 ? (
192
+ <ExplorerSection
193
+ section="pages"
194
+ models={grouped.pages}
195
+ workspace={workspace}
196
+ docsByType={docsByType}
197
+ route={route}
198
+ expanded={expanded}
199
+ onToggleFolder={toggleFolder}
200
+ onExpandFolderPath={expandFolderPath}
201
+ canWrite={canWrite}
202
+ />
203
+ ) : null}
204
+ {grouped.data.length > 0 ? (
205
+ <ExplorerSection
206
+ section="data"
207
+ models={grouped.data}
208
+ workspace={workspace}
209
+ docsByType={docsByType}
210
+ route={route}
211
+ expanded={expanded}
212
+ onToggleFolder={toggleFolder}
213
+ onExpandFolderPath={expandFolderPath}
214
+ canWrite={canWrite}
215
+ />
216
+ ) : null}
217
+ {grouped.globals.length > 0 ? (
218
+ <ExplorerSection
219
+ section="globals"
220
+ models={grouped.globals}
221
+ workspace={workspace}
222
+ docsByType={docsByType}
223
+ route={route}
224
+ expanded={expanded}
225
+ onToggleFolder={toggleFolder}
226
+ onExpandFolderPath={expandFolderPath}
227
+ canWrite={canWrite}
228
+ />
229
+ ) : null}
230
+ </>
231
+ )}
232
+ </div>
233
+ </div>
234
+ ) : null;
235
+
134
236
  return (
135
- <div {...stylex.props(styles.root)}>
237
+ <RailColumn>
136
238
  <ActivityRail
137
239
  pendingCount={pendingCount}
138
240
  settingsActive={settingsActive}
139
241
  workspaceSlug={workspace.workspaceSlug}
140
- contentRoute={route}
242
+ treeOpen={treeOpen}
141
243
  />
142
- {showTree ? <div {...stylex.props(styles.panel)}>
143
- <header {...stylex.props(styles.panelHeader)}>
144
- <span {...stylex.props(styles.panelTitle)}>Content</span>
145
- <div {...stylex.props(styles.panelActions)}>
146
- <IconButton label="New file (soon)" disabled>
147
- <ExplorerHeaderNewFileIcon />
148
- </IconButton>
149
- <IconButton label="View options (soon)" disabled>
150
- <ExplorerHeaderListIcon />
151
- </IconButton>
152
- </div>
153
- </header>
154
- <div {...stylex.props(styles.treeScroll)}>
155
- {empty ? (
156
- <p {...stylex.props(styles.emptyHint)}>
157
- No content types yet. Pull from GitHub to load models.
158
- </p>
159
- ) : (
160
- <>
161
- {grouped.pages.length > 0 ? (
162
- <ExplorerSection
163
- section="pages"
164
- models={grouped.pages}
165
- workspace={workspace}
166
- docsByType={docsByType}
167
- route={route}
168
- expanded={expanded}
169
- onToggleFolder={toggleFolder}
170
- onExpandFolderPath={expandFolderPath}
171
- canWrite={canWrite}
172
- />
173
- ) : null}
174
- {grouped.data.length > 0 ? (
175
- <ExplorerSection
176
- section="data"
177
- models={grouped.data}
178
- workspace={workspace}
179
- docsByType={docsByType}
180
- route={route}
181
- expanded={expanded}
182
- onToggleFolder={toggleFolder}
183
- onExpandFolderPath={expandFolderPath}
184
- canWrite={canWrite}
185
- />
186
- ) : null}
187
- {grouped.globals.length > 0 ? (
188
- <ExplorerSection
189
- section="globals"
190
- models={grouped.globals}
191
- workspace={workspace}
192
- docsByType={docsByType}
193
- route={route}
194
- expanded={expanded}
195
- onToggleFolder={toggleFolder}
196
- onExpandFolderPath={expandFolderPath}
197
- canWrite={canWrite}
198
- />
199
- ) : null}
200
- </>
201
- )}
202
- </div>
203
- </div> : null}
204
- </div>
244
+ {tree && treeHost ? createPortal(tree, treeHost) : null}
245
+ </RailColumn>
205
246
  );
206
247
  }
207
248
 
@@ -209,40 +250,40 @@ function ActivityRail({
209
250
  pendingCount,
210
251
  settingsActive,
211
252
  workspaceSlug,
212
- contentRoute,
253
+ treeOpen,
213
254
  }: {
214
255
  pendingCount: number;
215
256
  settingsActive: boolean;
216
257
  workspaceSlug: string;
217
- contentRoute: ContentRoute;
258
+ treeOpen: boolean;
218
259
  }) {
219
260
  const navigate = useNavigate();
220
261
  const location = useLocation();
262
+ const onDocument = isWorkspaceEditorPath(location.pathname);
263
+ const filesLabel = onDocument ? (treeOpen ? "Hide files" : "Show files") : "Content";
264
+ const filesActive = isWorkspaceContentPath(location.pathname) && (!onDocument || treeOpen);
221
265
 
222
266
  return (
223
267
  <nav
224
268
  aria-label="Studio"
225
- {...stylex.props(styles.rail, isWorkspaceMediaPath(location.pathname) && styles.railEdge)}
269
+ {...stylex.props(styles.rail)}
226
270
  >
227
271
  <div {...stylex.props(styles.railTop)}>
228
- <Tooltip label="Content" side="right">
272
+ <Tooltip label={filesLabel} side="right">
229
273
  <button
230
274
  type="button"
231
- aria-label="Content"
232
- aria-current={isWorkspaceContentPath(location.pathname) ? "page" : undefined}
275
+ aria-label={filesLabel}
276
+ aria-pressed={onDocument ? treeOpen : undefined}
277
+ aria-current={!onDocument && filesActive ? "page" : undefined}
233
278
  onClick={() => {
234
- if (isWorkspaceMediaPath(location.pathname)) {
235
- navigate(workspaceHomePath(workspaceSlug));
279
+ if (onDocument) {
280
+ toggleContentTree();
236
281
  return;
237
282
  }
238
- if (contentRoute.docSlug && contentRoute.type) {
239
- navigate(contentTypePath(workspaceSlug, contentRoute.type));
240
- }
283
+ const href = contentRailHref(location.pathname, workspaceSlug);
284
+ if (href) navigate(href);
241
285
  }}
242
- {...stylex.props(
243
- styles.railBtn,
244
- isWorkspaceContentPath(location.pathname) && styles.railBtnActive,
245
- )}
286
+ {...stylex.props(styles.railBtn, filesActive && styles.railBtnActive)}
246
287
  >
247
288
  <ExplorerRailFilesIcon />
248
289
  </button>
@@ -297,6 +338,7 @@ function ActivityRail({
297
338
  <ExplorerRailSettingsIcon />
298
339
  </Link>
299
340
  </Tooltip>
341
+ <UserMenu compact />
300
342
  </div>
301
343
  </nav>
302
344
  );
@@ -637,7 +679,7 @@ function FolderTreeRow({
637
679
  <ExplorerTwistie open={open} />
638
680
  </span>
639
681
  <span {...stylex.props(styles.treeIcon)}>
640
- <ExplorerFolderIcon open={open} />
682
+ <ExplorerFolderIcon />
641
683
  </span>
642
684
  <span {...stylex.props(styles.treeLabel)}>{node.name}</span>
643
685
  </button>
@@ -833,27 +875,32 @@ function sectionTitle(section: ContentTypeSection) {
833
875
  }
834
876
 
835
877
  const styles = stylex.create({
836
- root: {
878
+ railColumn: {
837
879
  display: "flex",
838
- minHeight: 0,
839
- flex: 1,
840
- flexDirection: "row",
880
+ height: "100%",
881
+ width: "48px",
882
+ minWidth: "48px",
883
+ flexShrink: 0,
884
+ flexDirection: "column",
885
+ borderRightWidth: 1,
886
+ borderRightStyle: "solid",
887
+ borderRightColor: colors.border,
888
+ backgroundColor: colors.bgSurface,
841
889
  },
842
890
  rail: {
843
891
  display: "flex",
844
892
  width: "48px",
893
+ minHeight: 0,
894
+ flex: 1,
845
895
  flexShrink: 0,
846
896
  flexDirection: "column",
847
897
  justifyContent: "space-between",
848
898
  alignItems: "center",
849
- borderRightWidth: 1,
850
- borderRightStyle: "solid",
851
- borderRightColor: colors.border,
852
899
  backgroundColor: colors.bgSidebar,
853
900
  paddingBlock: space.md,
854
901
  },
855
- railEdge: {
856
- borderRightWidth: 0,
902
+ railAccountOnly: {
903
+ justifyContent: "flex-end",
857
904
  },
858
905
  railTop: {
859
906
  display: "flex",
@@ -865,6 +912,7 @@ const styles = stylex.create({
865
912
  display: "flex",
866
913
  flexDirection: "column",
867
914
  alignItems: "center",
915
+ gap: 2,
868
916
  },
869
917
  railBtn: {
870
918
  position: "relative",
@@ -921,10 +969,15 @@ const styles = stylex.create({
921
969
  },
922
970
  panel: {
923
971
  display: "flex",
924
- minWidth: 0,
972
+ width: "calc(min(360px, 38vw) - 48px)",
973
+ minWidth: "252px",
925
974
  minHeight: 0,
926
- flex: 1,
975
+ flexGrow: 0,
976
+ flexShrink: 0,
927
977
  flexDirection: "column",
978
+ borderRightWidth: 1,
979
+ borderRightStyle: "solid",
980
+ borderRightColor: colors.border,
928
981
  backgroundColor: colors.bgSurface,
929
982
  },
930
983
  panelHeader: {
@@ -110,10 +110,12 @@ const styles = stylex.create({
110
110
  },
111
111
  },
112
112
  triggerCompact: {
113
+ width: "36px",
114
+ height: "36px",
115
+ minHeight: "36px",
113
116
  justifyContent: "center",
114
117
  paddingInline: 0,
115
118
  paddingBlock: 0,
116
- minHeight: "32px",
117
119
  },
118
120
  initial: {
119
121
  fontSize: type.sm,
@@ -0,0 +1,55 @@
1
+ // packages/studio/src/chrome/content-tree-visibility.ts
2
+ import { useEffect, useState } from "react";
3
+ import type { KeyValueStore } from "../last-workspace";
4
+ import { isWorkspaceContentPath, isWorkspaceEditorPath } from "../workspace-path";
5
+
6
+ export const CONTENT_TREE_OPEN_KEY = "backmatter-content-tree-open";
7
+ const CONTENT_TREE_EVENT = "backmatter-content-tree";
8
+
9
+ /**
10
+ * The type index is the file tree, so it stays open.
11
+ * A document follows the editor toggle. Other routes have no tree.
12
+ */
13
+ export function contentTreeVisible(pathname: string, open: boolean) {
14
+ if (!isWorkspaceContentPath(pathname)) return false;
15
+ if (isWorkspaceEditorPath(pathname)) return open;
16
+ return true;
17
+ }
18
+
19
+ export function readContentTreeOpen(storage: KeyValueStore = localStorage) {
20
+ try {
21
+ return storage.getItem(CONTENT_TREE_OPEN_KEY) !== "0";
22
+ } catch {
23
+ return true;
24
+ }
25
+ }
26
+
27
+ export function writeContentTreeOpen(
28
+ open: boolean,
29
+ storage: KeyValueStore = localStorage,
30
+ ) {
31
+ try {
32
+ storage.setItem(CONTENT_TREE_OPEN_KEY, open ? "1" : "0");
33
+ } catch {
34
+ // ignore quota / private mode
35
+ }
36
+ }
37
+
38
+ export function toggleContentTree() {
39
+ writeContentTreeOpen(!readContentTreeOpen());
40
+ window.dispatchEvent(new Event(CONTENT_TREE_EVENT));
41
+ }
42
+
43
+ export function useContentTreeOpen() {
44
+ const [open, setOpen] = useState(() => readContentTreeOpen());
45
+ useEffect(() => {
46
+ const sync = () => setOpen(readContentTreeOpen());
47
+ window.addEventListener(CONTENT_TREE_EVENT, sync);
48
+ window.addEventListener("storage", sync);
49
+ return () => {
50
+ window.removeEventListener(CONTENT_TREE_EVENT, sync);
51
+ window.removeEventListener("storage", sync);
52
+ };
53
+ }, []);
54
+ return open;
55
+ }
@@ -9,7 +9,6 @@ import {
9
9
  IconFileText,
10
10
  IconFiles,
11
11
  IconFolder1,
12
- IconFolderOpen,
13
12
  IconGit,
14
13
  IconHistory,
15
14
  IconImages1,
@@ -42,9 +41,8 @@ export function ExplorerTwistie({ open }: { open: boolean }) {
42
41
  return <Central icon={Icon} size={TREE} />;
43
42
  }
44
43
 
45
- export function ExplorerFolderIcon({ open }: { open: boolean }) {
46
- const Icon = open ? IconFolderOpen : IconFolder1;
47
- return <Central icon={Icon} size={TREE} />;
44
+ export function ExplorerFolderIcon() {
45
+ return <Central icon={IconFolder1} size={TREE} />;
48
46
  }
49
47
 
50
48
  export function ExplorerFileIcon({ ext }: { ext?: string }) {