@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
@@ -9,16 +9,15 @@ import {
9
9
  unbindGithub,
10
10
  } from "../api";
11
11
  import { Button, ButtonLink } from "../ui/Button";
12
- import { Field } from "../ui/Field";
13
12
  import { Input } from "../ui/Input";
14
13
  import { ConfirmDialog } from "../ui/ConfirmDialog";
15
- import { PageHeader, Panel, Stack } from "../ui/Page";
14
+ import { PageHeader } from "../ui/Page";
16
15
  import { Text } from "../ui/Text";
17
16
  import * as stylex from "@stylexjs/stylex";
18
17
  import { colors, space, type } from "../styles/tokens.stylex";
19
18
  import type { WorkspaceOutletContext } from "../workspace/types";
20
19
  import { githubConnectPath } from "../workspace-path";
21
- import { SelectRepositoriesLink } from "./SelectRepositoriesLink";
20
+ import { GithubRepositoryAccess } from "./SelectRepositoriesLink";
22
21
 
23
22
  export function SettingsGithubPage() {
24
23
  const { workspace, reload } = useOutletContext<WorkspaceOutletContext>();
@@ -34,6 +33,7 @@ export function SettingsGithubPage() {
34
33
  const [directory, setDirectory] = useState(workspace.githubDirectory ?? "");
35
34
  const [directoryError, setDirectoryError] = useState<string | null>(null);
36
35
  const [savingDirectory, setSavingDirectory] = useState(false);
36
+ const directoryDirty = directory !== (workspace.githubDirectory ?? "");
37
37
 
38
38
  useEffect(() => {
39
39
  setDirectory(workspace.githubDirectory ?? "");
@@ -119,121 +119,36 @@ export function SettingsGithubPage() {
119
119
  <Text tone="danger">{error}</Text>
120
120
  </div>
121
121
  ) : null}
122
- <Panel>
123
- {connected ? (
124
- <Stack>
125
- <dl {...stylex.props(styles.dl)}>
126
- <div>
127
- <dt {...stylex.props(styles.dt)}>Repository</dt>
128
- <dd {...stylex.props(styles.dd)}>{workspace.githubRepoFullName}</dd>
129
- </div>
130
- <div>
131
- <dt {...stylex.props(styles.dt)}>Branch</dt>
132
- <dd {...stylex.props(styles.dd)}>
133
- {workspace.githubDefaultBranch}
134
- </dd>
135
- </div>
136
- {!canEdit ? (
137
- <div>
138
- <dt {...stylex.props(styles.dt)}>Directory</dt>
139
- <dd {...stylex.props(styles.dd)}>
140
- {workspace.githubDirectory ?? "Repository root"}
141
- </dd>
142
- </div>
143
- ) : null}
144
- </dl>
145
- {canEdit ? (
146
- <form onSubmit={(e) => void onSaveDirectory(e)}>
147
- <Stack>
148
- <Field
149
- label="Directory"
150
- hint="Folder inside the repository that contains this site. Leave empty for the repository root."
151
- >
152
- <Input
153
- value={directory}
154
- onChange={(e) => setDirectory(e.target.value)}
155
- />
156
- </Field>
157
- {directoryError ? (
158
- <Text tone="danger">{directoryError}</Text>
159
- ) : null}
160
- <div>
161
- <Button type="submit" disabled={savingDirectory}>
162
- {savingDirectory ? "Saving…" : "Save"}
163
- </Button>
164
- </div>
165
- </Stack>
166
- </form>
167
- ) : null}
168
- {canEdit ? (
169
- <div {...stylex.props(styles.actions)}>
170
- <ButtonLink
171
- variant="secondary"
172
- to={githubConnectPath(workspace.slug)}
173
- >
174
- Change repository
175
- </ButtonLink>
176
- <SelectRepositoriesLink workspaceId={workspace.id} />
177
- <Button
178
- variant="danger"
179
- disabled={disconnecting}
180
- onClick={() => setConfirmDisconnect(true)}
181
- >
182
- Disconnect
183
- </Button>
184
- </div>
185
- ) : null}
186
- </Stack>
187
- ) : (
188
- <Stack>
189
- <Text tone="muted" size="sm">
190
- {workspace.githubInstallationId
191
- ? "The list only includes repositories this GitHub App can access. Select repositories on GitHub to grant more. Saving there does not return to Backmatter."
192
- : "Connect finds the backmatter CMS installation on your GitHub account and opens the repository list here. Saving on GitHub does not return to Backmatter."}
193
- </Text>
194
- {canEdit ? (
195
- <div {...stylex.props(styles.actions)}>
196
- {workspace.githubInstallationId ? (
197
- <ButtonLink to={githubConnectPath(workspace.slug)}>
198
- Choose repository
199
- </ButtonLink>
200
- ) : choices ? (
201
- choices.map((choice) => (
202
- <Button
203
- key={choice.id}
204
- disabled={connecting}
205
- onClick={() => void onConnect(choice.id)}
206
- >
207
- {choice.account ?? choice.id}
208
- </Button>
209
- ))
210
- ) : (
211
- <Button disabled={connecting} onClick={() => void onConnect()}>
212
- {connecting ? "Connecting…" : "Connect GitHub"}
213
- </Button>
214
- )}
215
- {workspace.githubInstallationId ? (
216
- <SelectRepositoriesLink workspaceId={workspace.id} />
217
- ) : null}
218
- {installUrl ? (
219
- <ButtonLink href={installUrl} newTab={false} variant="secondary">
220
- Install on GitHub
221
- </ButtonLink>
222
- ) : null}
223
- {workspace.githubInstallationId ? (
224
- <Button
225
- variant="danger"
226
- disabled={disconnecting}
227
- onClick={() => setConfirmDisconnect(true)}
228
- >
229
- Disconnect
230
- </Button>
231
- ) : null}
232
- </div>
233
- ) : null}
234
- </Stack>
235
- )}
236
- </Panel>
122
+ {connected ? (
123
+ <ConnectedGithub
124
+ repo={workspace.githubRepoFullName ?? ""}
125
+ branch={workspace.githubDefaultBranch ?? ""}
126
+ directory={directory}
127
+ directoryDirty={directoryDirty}
128
+ directoryError={directoryError}
129
+ savingDirectory={savingDirectory}
130
+ canEdit={canEdit}
131
+ changeTo={githubConnectPath(workspace.slug)}
132
+ workspaceId={workspace.id}
133
+ disconnecting={disconnecting}
134
+ onDirectoryChange={setDirectory}
135
+ onSaveDirectory={(e) => void onSaveDirectory(e)}
136
+ onDisconnect={() => setConfirmDisconnect(true)}
137
+ />
138
+ ) : (
139
+ <DisconnectedGithub
140
+ canEdit={canEdit}
141
+ connecting={connecting}
142
+ choices={choices}
143
+ installUrl={installUrl}
144
+ hasInstallation={Boolean(workspace.githubInstallationId)}
145
+ chooseTo={githubConnectPath(workspace.slug)}
146
+ workspaceId={workspace.id}
147
+ disconnecting={disconnecting}
148
+ onConnect={(id) => void onConnect(id)}
149
+ onDisconnect={() => setConfirmDisconnect(true)}
150
+ />
151
+ )}
237
152
  <ConfirmDialog
238
153
  open={confirmDisconnect}
239
154
  onOpenChange={(open) => {
@@ -254,28 +169,402 @@ export function SettingsGithubPage() {
254
169
  );
255
170
  }
256
171
 
172
+ function ConnectedGithub({
173
+ repo,
174
+ branch,
175
+ directory,
176
+ directoryDirty,
177
+ directoryError,
178
+ savingDirectory,
179
+ canEdit,
180
+ changeTo,
181
+ workspaceId,
182
+ disconnecting,
183
+ onDirectoryChange,
184
+ onSaveDirectory,
185
+ onDisconnect,
186
+ }: {
187
+ repo: string;
188
+ branch: string;
189
+ directory: string;
190
+ directoryDirty: boolean;
191
+ directoryError: string | null;
192
+ savingDirectory: boolean;
193
+ canEdit: boolean;
194
+ changeTo: string;
195
+ workspaceId: string;
196
+ disconnecting: boolean;
197
+ onDirectoryChange: (value: string) => void;
198
+ onSaveDirectory: (e: React.FormEvent) => void;
199
+ onDisconnect: () => void;
200
+ }) {
201
+ return (
202
+ <div {...stylex.props(styles.groups)}>
203
+ <SettingsGroup title="Connection" titleId="github-connection">
204
+ <SettingsRow
205
+ label="Repository"
206
+ control={
207
+ <>
208
+ <span {...stylex.props(styles.value)}>{repo}</span>
209
+ {canEdit ? (
210
+ <ButtonLink variant="secondary" to={changeTo}>
211
+ Change
212
+ </ButtonLink>
213
+ ) : null}
214
+ </>
215
+ }
216
+ />
217
+ <SettingsRow
218
+ label="Branch"
219
+ control={<span {...stylex.props(styles.value)}>{branch}</span>}
220
+ />
221
+ <SettingsRow
222
+ label="Directory"
223
+ htmlFor={canEdit ? "github-directory" : undefined}
224
+ description={
225
+ <>
226
+ Folder inside the repository. Empty means the repository root.
227
+ {directoryError ? <Text tone="danger">{directoryError}</Text> : null}
228
+ </>
229
+ }
230
+ control={
231
+ canEdit ? (
232
+ <form onSubmit={onSaveDirectory} {...stylex.props(styles.directoryForm)}>
233
+ <div {...stylex.props(styles.directoryField)}>
234
+ <Input
235
+ id="github-directory"
236
+ value={directory}
237
+ spellCheck={false}
238
+ autoCapitalize="off"
239
+ autoCorrect="off"
240
+ aria-invalid={directoryError ? true : undefined}
241
+ onChange={(e) => onDirectoryChange(e.target.value)}
242
+ />
243
+ </div>
244
+ {directoryDirty ? (
245
+ <Button type="submit" disabled={savingDirectory}>
246
+ {savingDirectory ? "Saving…" : "Save"}
247
+ </Button>
248
+ ) : null}
249
+ </form>
250
+ ) : (
251
+ <span {...stylex.props(styles.value)}>
252
+ {directory || "Repository root"}
253
+ </span>
254
+ )
255
+ }
256
+ />
257
+ </SettingsGroup>
258
+ {canEdit ? (
259
+ <SettingsGroup title="Access" titleId="github-access">
260
+ <SettingsRow
261
+ label="Repository access"
262
+ description="Repositories the app can see on GitHub. Saving there does not return here."
263
+ control={
264
+ <GithubRepositoryAccess workspaceId={workspaceId}>
265
+ {(url) => (
266
+ <ButtonLink href={url} variant="secondary">
267
+ Manage on GitHub
268
+ </ButtonLink>
269
+ )}
270
+ </GithubRepositoryAccess>
271
+ }
272
+ />
273
+ </SettingsGroup>
274
+ ) : null}
275
+ {canEdit ? (
276
+ <SettingsGroup separated>
277
+ <SettingsRow
278
+ label="Disconnect"
279
+ description="Stops commits and imports. Content already here stays."
280
+ control={
281
+ <Button variant="danger" disabled={disconnecting} onClick={onDisconnect}>
282
+ Disconnect
283
+ </Button>
284
+ }
285
+ />
286
+ </SettingsGroup>
287
+ ) : null}
288
+ </div>
289
+ );
290
+ }
291
+
292
+ function DisconnectedGithub({
293
+ canEdit,
294
+ connecting,
295
+ choices,
296
+ installUrl,
297
+ hasInstallation,
298
+ chooseTo,
299
+ workspaceId,
300
+ disconnecting,
301
+ onConnect,
302
+ onDisconnect,
303
+ }: {
304
+ canEdit: boolean;
305
+ connecting: boolean;
306
+ choices: GithubInstallationChoice[] | null;
307
+ installUrl: string | null;
308
+ hasInstallation: boolean;
309
+ chooseTo: string;
310
+ workspaceId: string;
311
+ disconnecting: boolean;
312
+ onConnect: (installationId?: string) => void;
313
+ onDisconnect: () => void;
314
+ }) {
315
+ if (choices) {
316
+ return (
317
+ <SettingsGroup
318
+ title="GitHub accounts"
319
+ titleId="github-accounts"
320
+ description="Connect finds the backmatter CMS installation on your GitHub account and opens the repository list here. Saving on GitHub does not return to Backmatter."
321
+ >
322
+ {choices.map((choice) => (
323
+ <SettingsRow
324
+ key={choice.id}
325
+ label={choice.account ?? choice.id}
326
+ control={
327
+ canEdit ? (
328
+ <Button
329
+ variant="secondary"
330
+ disabled={connecting}
331
+ onClick={() => onConnect(choice.id)}
332
+ >
333
+ Connect
334
+ </Button>
335
+ ) : null
336
+ }
337
+ />
338
+ ))}
339
+ </SettingsGroup>
340
+ );
341
+ }
342
+
343
+ if (hasInstallation) {
344
+ return (
345
+ <div {...stylex.props(styles.groups)}>
346
+ <SettingsGroup title="Connection" titleId="github-connection">
347
+ <SettingsRow
348
+ label="Repository"
349
+ description="The list only includes repositories this GitHub App can access."
350
+ control={
351
+ canEdit ? (
352
+ <ButtonLink to={chooseTo}>Choose repository</ButtonLink>
353
+ ) : null
354
+ }
355
+ />
356
+ </SettingsGroup>
357
+ {canEdit ? (
358
+ <SettingsGroup title="Access" titleId="github-access">
359
+ <SettingsRow
360
+ label="Repository access"
361
+ description="Grant more repositories on GitHub. Saving there does not return here."
362
+ control={
363
+ <GithubRepositoryAccess workspaceId={workspaceId}>
364
+ {(url) => (
365
+ <ButtonLink href={url} variant="secondary">
366
+ Manage on GitHub
367
+ </ButtonLink>
368
+ )}
369
+ </GithubRepositoryAccess>
370
+ }
371
+ />
372
+ </SettingsGroup>
373
+ ) : null}
374
+ {canEdit ? (
375
+ <SettingsGroup separated>
376
+ <SettingsRow
377
+ label="Disconnect"
378
+ description="Removes the GitHub App from this workspace. You can connect again later."
379
+ control={
380
+ <Button
381
+ variant="danger"
382
+ disabled={disconnecting}
383
+ onClick={onDisconnect}
384
+ >
385
+ Disconnect
386
+ </Button>
387
+ }
388
+ />
389
+ </SettingsGroup>
390
+ ) : null}
391
+ </div>
392
+ );
393
+ }
394
+
395
+ return (
396
+ <SettingsGroup>
397
+ <SettingsRow
398
+ label="Not connected"
399
+ description="Connect finds the backmatter CMS installation on your GitHub account and opens the repository list here. Saving on GitHub does not return to Backmatter."
400
+ control={
401
+ canEdit ? (
402
+ installUrl ? (
403
+ <ButtonLink href={installUrl} newTab={false}>
404
+ Install on GitHub
405
+ </ButtonLink>
406
+ ) : (
407
+ <Button disabled={connecting} onClick={() => onConnect()}>
408
+ {connecting ? "Connecting…" : "Connect GitHub"}
409
+ </Button>
410
+ )
411
+ ) : null
412
+ }
413
+ />
414
+ </SettingsGroup>
415
+ );
416
+ }
417
+
418
+ function SettingsGroup({
419
+ title,
420
+ titleId,
421
+ description,
422
+ separated,
423
+ children,
424
+ }: {
425
+ title?: string;
426
+ titleId?: string;
427
+ description?: string;
428
+ separated?: boolean;
429
+ children: React.ReactNode;
430
+ }) {
431
+ return (
432
+ <section
433
+ aria-labelledby={title ? titleId : undefined}
434
+ {...stylex.props(styles.group, separated && styles.groupSeparated)}
435
+ >
436
+ {title && titleId ? (
437
+ <h2 id={titleId} {...stylex.props(styles.groupTitle)}>
438
+ {title}
439
+ </h2>
440
+ ) : null}
441
+ {description ? (
442
+ <p {...stylex.props(styles.groupDescription)}>{description}</p>
443
+ ) : null}
444
+ <div {...stylex.props(styles.rows)}>{children}</div>
445
+ </section>
446
+ );
447
+ }
448
+
449
+ function SettingsRow({
450
+ label,
451
+ description,
452
+ htmlFor,
453
+ control,
454
+ }: {
455
+ label: string;
456
+ description?: React.ReactNode;
457
+ htmlFor?: string;
458
+ control?: React.ReactNode;
459
+ }) {
460
+ return (
461
+ <div {...stylex.props(styles.row)}>
462
+ <div {...stylex.props(styles.rowText)}>
463
+ {htmlFor ? (
464
+ <label htmlFor={htmlFor} {...stylex.props(styles.rowLabel)}>
465
+ {label}
466
+ </label>
467
+ ) : (
468
+ <div {...stylex.props(styles.rowLabel)}>{label}</div>
469
+ )}
470
+ {description ? (
471
+ <div {...stylex.props(styles.rowDescription)}>{description}</div>
472
+ ) : null}
473
+ </div>
474
+ {control ? <div {...stylex.props(styles.rowControl)}>{control}</div> : null}
475
+ </div>
476
+ );
477
+ }
478
+
257
479
  const styles = stylex.create({
258
- dl: {
480
+ groups: {
481
+ display: "flex",
482
+ flexDirection: "column",
483
+ gap: space.xxl,
484
+ },
485
+ group: {
486
+ display: "flex",
487
+ flexDirection: "column",
488
+ gap: space.xs,
489
+ },
490
+ groupSeparated: {
491
+ paddingTop: space.lg,
492
+ borderTopWidth: 1,
493
+ borderTopStyle: "solid",
494
+ borderTopColor: colors.border,
495
+ },
496
+ groupTitle: {
259
497
  margin: 0,
498
+ fontSize: type.sm,
499
+ fontWeight: 500,
500
+ color: colors.textMuted,
501
+ },
502
+ groupDescription: {
503
+ margin: 0,
504
+ color: colors.textMuted,
505
+ fontSize: type.sm,
506
+ lineHeight: 1.45,
507
+ },
508
+ rows: {
260
509
  display: "flex",
261
510
  flexDirection: "column",
511
+ },
512
+ row: {
513
+ display: "flex",
514
+ alignItems: "center",
515
+ justifyContent: "space-between",
516
+ flexWrap: "wrap",
262
517
  gap: space.lg,
518
+ minHeight: "44px",
519
+ paddingBlock: space.lg,
520
+ borderBottomWidth: 1,
521
+ borderBottomStyle: "solid",
522
+ borderBottomColor: colors.border,
523
+ ":last-child": {
524
+ borderBottomWidth: 0,
525
+ },
526
+ },
527
+ rowText: {
528
+ flex: "1 1 16rem",
529
+ minWidth: 0,
530
+ display: "flex",
531
+ flexDirection: "column",
532
+ gap: space.xs,
533
+ },
534
+ rowLabel: {
263
535
  fontSize: type.md,
536
+ fontWeight: 500,
537
+ color: colors.text,
264
538
  },
265
- dt: {
539
+ rowDescription: {
540
+ display: "flex",
541
+ flexDirection: "column",
542
+ gap: space.xs,
266
543
  color: colors.textMuted,
267
544
  fontSize: type.sm,
545
+ lineHeight: 1.45,
268
546
  },
269
- dd: {
270
- margin: 0,
547
+ rowControl: {
548
+ display: "flex",
549
+ alignItems: "center",
550
+ justifyContent: "flex-end",
551
+ flexShrink: 0,
552
+ gap: space.sm,
553
+ marginLeft: "auto",
554
+ },
555
+ value: {
556
+ fontSize: type.md,
271
557
  fontWeight: 500,
558
+ color: colors.text,
272
559
  },
273
- actions: {
560
+ directoryForm: {
274
561
  display: "flex",
275
- flexWrap: "wrap",
276
562
  alignItems: "center",
277
563
  gap: space.sm,
278
564
  },
565
+ directoryField: {
566
+ width: "240px",
567
+ },
279
568
  error: {
280
569
  marginBottom: space.lg,
281
570
  },
@@ -16,13 +16,18 @@ const SURFACES: { key: keyof WorkspaceUsage["dataRequests"]; label: string }[] =
16
16
  { key: "ai", label: "Assistant" },
17
17
  ];
18
18
 
19
+ const IMAGES: { key: "transformations" | "cachedServes" | "uncachedServes"; label: string }[] = [
20
+ { key: "transformations", label: "Transformations" },
21
+ { key: "cachedServes", label: "Cached serves" },
22
+ { key: "uncachedServes", label: "Uncached serves" },
23
+ ];
24
+
19
25
  const WORK: { key: keyof WorkspaceUsage["work"]; label: string }[] = [
20
26
  { key: "commits", label: "Commits" },
21
27
  { key: "commitFiles", label: "Files committed" },
22
28
  { key: "imports", label: "Imports" },
23
29
  { key: "importFiles", label: "Files imported" },
24
30
  { key: "saves", label: "Saves" },
25
- { key: "mediaTransforms", label: "Image transforms" },
26
31
  ];
27
32
 
28
33
  function formatBytes(value: number): string {
@@ -128,6 +133,23 @@ export function SettingsUsagePage() {
128
133
  </div>
129
134
  </dl>
130
135
  </Panel>
136
+ <Panel>
137
+ <Text as="h2" size="lg" weight="medium">
138
+ Images
139
+ </Text>
140
+ <dl {...stylex.props(styles.list)}>
141
+ {IMAGES.map((item) => (
142
+ <div key={item.key} {...stylex.props(styles.row)}>
143
+ <dt {...stylex.props(styles.label)}>{item.label}</dt>
144
+ <dd {...stylex.props(styles.value)}>{formatCount(usage.images[item.key])}</dd>
145
+ </div>
146
+ ))}
147
+ <div {...stylex.props(styles.row)}>
148
+ <dt {...stylex.props(styles.label)}>Transfer</dt>
149
+ <dd {...stylex.props(styles.value)}>{formatBytes(usage.images.bytes)}</dd>
150
+ </div>
151
+ </dl>
152
+ </Panel>
131
153
  <Panel>
132
154
  <Text as="h2" size="lg" weight="medium">
133
155
  AI
@@ -8,6 +8,8 @@ export type CatalogMedia = {
8
8
  thumbUrl: string;
9
9
  width: number | null;
10
10
  height: number | null;
11
+ alt?: string;
12
+ caption?: string;
11
13
  contentType?: string;
12
14
  byteSize?: number;
13
15
  createdAt?: string;
@@ -128,6 +130,17 @@ export function receivePage(
128
130
  };
129
131
  }
130
132
 
133
+ /** The saved alt text and caption replace that image wherever the grid is holding it. */
134
+ export function describeMedia(state: MediaGridState, media: CatalogMedia): MediaGridState {
135
+ return {
136
+ ...state,
137
+ pending: state.pending.map((item) =>
138
+ item.media?.id === media.id ? { ...item, media } : item,
139
+ ),
140
+ loaded: state.loaded.map((item) => (item.id === media.id ? media : item)),
141
+ };
142
+ }
143
+
131
144
  /** Drop a hidden row from the grid. A just-finished upload leaves its cell too. */
132
145
  export function removeMedia(state: MediaGridState, id: string): MediaGridState {
133
146
  const pending = state.pending.filter((item) => item.media?.id !== id);
@@ -142,7 +155,7 @@ export function removeMedia(state: MediaGridState, id: string): MediaGridState {
142
155
  };
143
156
  }
144
157
 
145
- /** Cells the grid paints: in-flight uploads, then known rows, then empty slots for the rest. */
158
+ /** Uploads plus catalog rows, including rows that have not been fetched yet. */
146
159
  export function gridLength(state: MediaGridState): number {
147
160
  const held = state.pending.filter((item) => item.media).length;
148
161
  const catalog = Math.max(state.total - held, state.loaded.length);