@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,460 @@
1
+ // packages/studio/src/pages/MediaLightbox.tsx
2
+ import { Menu } from "@base-ui/react/menu";
3
+ import {
4
+ IconSquareBehindSquare1,
5
+ IconTrashCanSimple,
6
+ } from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
7
+ import { useEffect, useState } from "react";
8
+ import * as stylex from "@stylexjs/stylex";
9
+ import { colors, radius, space, type, z } from "../styles/tokens.stylex";
10
+ import { Text } from "../ui/Text";
11
+ import { cx, popup, popupClassName } from "../ui/popup";
12
+ import type { CatalogMedia } from "./media-grid";
13
+
14
+ const stack = "@media (max-width: 800px)";
15
+ const hover = "@media (hover: hover)";
16
+
17
+ export function MediaLightbox({
18
+ item,
19
+ index,
20
+ count,
21
+ onClose,
22
+ onPrev,
23
+ onNext,
24
+ onDelete,
25
+ }: {
26
+ item: CatalogMedia;
27
+ index: number;
28
+ count: number;
29
+ onClose: () => void;
30
+ onPrev: () => void;
31
+ onNext: () => void;
32
+ onDelete?: () => void;
33
+ }) {
34
+ const full = item.variants?.["1200"] ?? item.url;
35
+ const [copied, setCopied] = useState(false);
36
+
37
+ useEffect(() => {
38
+ setCopied(false);
39
+ }, [item.id]);
40
+
41
+ useEffect(() => {
42
+ const onKey = (event: KeyboardEvent) => {
43
+ if (document.querySelector('[role="menu"], [role="alertdialog"]')) return;
44
+ if (event.key === "Escape" || event.key === " ") {
45
+ event.preventDefault();
46
+ onClose();
47
+ }
48
+ if (event.key === "ArrowLeft") {
49
+ event.preventDefault();
50
+ onPrev();
51
+ }
52
+ if (event.key === "ArrowRight") {
53
+ event.preventDefault();
54
+ onNext();
55
+ }
56
+ };
57
+ window.addEventListener("keydown", onKey, true);
58
+ return () => window.removeEventListener("keydown", onKey, true);
59
+ }, [onClose, onPrev, onNext]);
60
+
61
+ return (
62
+ <div
63
+ {...stylex.props(styles.shell)}
64
+ role="dialog"
65
+ aria-modal="true"
66
+ aria-label={item.filename}
67
+ >
68
+ <div {...stylex.props(styles.stage)} onClick={onClose}>
69
+ <div {...stylex.props(styles.stageTools)} onClick={(event) => event.stopPropagation()}>
70
+ <Menu.Root>
71
+ <Menu.Trigger aria-label="Image actions" className={cx(styles.stageTool)}>
72
+ <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
73
+ <circle cx="3" cy="7" r="1.15" fill="currentColor" />
74
+ <circle cx="7" cy="7" r="1.15" fill="currentColor" />
75
+ <circle cx="11" cy="7" r="1.15" fill="currentColor" />
76
+ </svg>
77
+ </Menu.Trigger>
78
+ <Menu.Portal>
79
+ <Menu.Positioner side="bottom" align="end" sideOffset={6} className={cx(popup.positioner)}>
80
+ <Menu.Popup
81
+ aria-label="Image actions"
82
+ className={(state) => popupClassName(state, styles.menu)}
83
+ >
84
+ <Menu.Item
85
+ label="Copy path"
86
+ onClick={() => {
87
+ void navigator.clipboard.writeText(item.url);
88
+ }}
89
+ className={(state) =>
90
+ cx(popup.item, styles.menuItem, state.highlighted && popup.itemHighlighted)
91
+ }
92
+ >
93
+ <IconSquareBehindSquare1 size={14} ariaHidden style={{ flexShrink: 0 }} />
94
+ Copy path
95
+ </Menu.Item>
96
+ {onDelete ? (
97
+ <Menu.Item
98
+ label="Delete"
99
+ onClick={onDelete}
100
+ className={(state) =>
101
+ cx(popup.item, styles.menuItem, styles.menuDanger, state.highlighted && popup.itemHighlighted)
102
+ }
103
+ >
104
+ <IconTrashCanSimple size={14} ariaHidden style={{ flexShrink: 0 }} />
105
+ Delete
106
+ </Menu.Item>
107
+ ) : null}
108
+ </Menu.Popup>
109
+ </Menu.Positioner>
110
+ </Menu.Portal>
111
+ </Menu.Root>
112
+ <button
113
+ type="button"
114
+ aria-label="Close"
115
+ {...stylex.props(styles.stageTool)}
116
+ onClick={onClose}
117
+ >
118
+ <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
119
+ <path
120
+ d="M3.5 3.5l7 7M10.5 3.5l-7 7"
121
+ fill="none"
122
+ stroke="currentColor"
123
+ strokeWidth="1.5"
124
+ strokeLinecap="round"
125
+ />
126
+ </svg>
127
+ </button>
128
+ </div>
129
+ <button
130
+ type="button"
131
+ aria-label="Previous image"
132
+ disabled={index <= 0}
133
+ {...stylex.props(styles.nav, styles.navPrev)}
134
+ onClick={(event) => {
135
+ event.stopPropagation();
136
+ onPrev();
137
+ }}
138
+ >
139
+ ‹
140
+ </button>
141
+ <div {...stylex.props(styles.frame)} onClick={(event) => event.stopPropagation()}>
142
+ <StageImage key={item.id} thumb={item.thumbUrl} full={full} alt={item.filename} />
143
+ </div>
144
+ <button
145
+ type="button"
146
+ aria-label="Next image"
147
+ disabled={index >= count - 1}
148
+ {...stylex.props(styles.nav, styles.navNext)}
149
+ onClick={(event) => {
150
+ event.stopPropagation();
151
+ onNext();
152
+ }}
153
+ >
154
+ ›
155
+ </button>
156
+ </div>
157
+ <aside {...stylex.props(styles.sidebar)}>
158
+ <div {...stylex.props(styles.filename)}>
159
+ <Text as="h2" size="lg" weight="medium">
160
+ {item.filename}
161
+ </Text>
162
+ </div>
163
+ <dl {...stylex.props(styles.facts)}>
164
+ <Fact label="Dimensions" value={dimensions(item)} />
165
+ <Fact label="Size" value={item.byteSize === undefined ? "—" : formatBytes(item.byteSize)} />
166
+ <Fact label="Type" value={kindLabel(item.contentType)} />
167
+ <Fact label="Added" value={addedLabel(item.createdAt)} />
168
+ </dl>
169
+ <div {...stylex.props(styles.urlBlock)}>
170
+ <div {...stylex.props(styles.urlHead)}>
171
+ <Text size="xs" tone="muted">
172
+ Original URL
173
+ </Text>
174
+ <button
175
+ type="button"
176
+ aria-label={copied ? "Copied" : "Copy URL"}
177
+ {...stylex.props(styles.copy)}
178
+ onClick={() => {
179
+ void navigator.clipboard.writeText(item.url).then(() => setCopied(true));
180
+ }}
181
+ >
182
+ <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
183
+ <rect
184
+ x="4.75"
185
+ y="4.75"
186
+ width="7"
187
+ height="7"
188
+ rx="1.25"
189
+ fill="none"
190
+ stroke="currentColor"
191
+ strokeWidth="1.5"
192
+ />
193
+ <path
194
+ d="M9.25 4.75V3.25A1.25 1.25 0 0 0 8 2H3.25A1.25 1.25 0 0 0 2 3.25V8A1.25 1.25 0 0 0 3.25 9.25H4.75"
195
+ fill="none"
196
+ stroke="currentColor"
197
+ strokeWidth="1.5"
198
+ />
199
+ </svg>
200
+ {copied ? "Copied" : "Copy"}
201
+ </button>
202
+ </div>
203
+ <p {...stylex.props(styles.url)}>{item.url}</p>
204
+ </div>
205
+ </aside>
206
+ </div>
207
+ );
208
+ }
209
+
210
+ function StageImage({ thumb, full, alt }: { thumb: string; full: string; alt: string }) {
211
+ const [ready, setReady] = useState(false);
212
+ return (
213
+ <>
214
+ <img src={thumb} alt="" {...stylex.props(styles.photo, styles.thumb)} />
215
+ <img
216
+ src={full}
217
+ alt={alt}
218
+ {...stylex.props(styles.photo, ready ? styles.shown : styles.hidden)}
219
+ onLoad={() => setReady(true)}
220
+ />
221
+ </>
222
+ );
223
+ }
224
+
225
+ function Fact({ label, value }: { label: string; value: string }) {
226
+ return (
227
+ <div {...stylex.props(styles.fact)}>
228
+ <dt {...stylex.props(styles.factLabel)}>{label}</dt>
229
+ <dd {...stylex.props(styles.factValue)}>{value}</dd>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function dimensions(item: CatalogMedia): string {
235
+ if (item.width === null || item.height === null || !item.width || !item.height) return "—";
236
+ return `${item.width} × ${item.height}`;
237
+ }
238
+
239
+ function kindLabel(contentType: string | undefined): string {
240
+ if (!contentType) return "—";
241
+ const subtype = contentType.split("/")[1] ?? contentType;
242
+ return subtype.toUpperCase();
243
+ }
244
+
245
+ function addedLabel(createdAt: string | undefined): string {
246
+ if (!createdAt) return "—";
247
+ const date = new Date(createdAt);
248
+ if (Number.isNaN(date.getTime())) return "—";
249
+ return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
250
+ }
251
+
252
+ function formatBytes(value: number): string {
253
+ if (value < 1024) return `${value} B`;
254
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
255
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MB`;
256
+ return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
257
+ }
258
+
259
+ const styles = stylex.create({
260
+ shell: {
261
+ position: "fixed",
262
+ inset: 0,
263
+ zIndex: z.palette,
264
+ display: "flex",
265
+ flexDirection: "row",
266
+ backgroundColor: "#0c0c0e",
267
+ [stack]: {
268
+ flexDirection: "column",
269
+ },
270
+ },
271
+ stage: {
272
+ position: "relative",
273
+ flex: 1,
274
+ minWidth: 0,
275
+ minHeight: 0,
276
+ display: "flex",
277
+ alignItems: "center",
278
+ justifyContent: "center",
279
+ [stack]: {
280
+ minHeight: "55vh",
281
+ },
282
+ },
283
+ frame: {
284
+ position: "relative",
285
+ width: "100%",
286
+ height: "100%",
287
+ },
288
+ photo: {
289
+ position: "absolute",
290
+ inset: 0,
291
+ width: "100%",
292
+ height: "100%",
293
+ objectFit: "contain",
294
+ padding: space.xxl,
295
+ boxSizing: "border-box",
296
+ },
297
+ thumb: {
298
+ opacity: 1,
299
+ },
300
+ shown: {
301
+ opacity: 1,
302
+ },
303
+ hidden: {
304
+ opacity: 0,
305
+ },
306
+ nav: {
307
+ position: "absolute",
308
+ top: "50%",
309
+ transform: "translateY(-50%)",
310
+ zIndex: 1,
311
+ width: "44px",
312
+ height: "44px",
313
+ borderWidth: 0,
314
+ borderRadius: radius.md,
315
+ backgroundColor: "rgba(255, 255, 255, 0.12)",
316
+ color: "#fafafa",
317
+ fontSize: "28px",
318
+ display: "flex",
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ cursor: "pointer",
322
+ ":disabled": {
323
+ opacity: 0.35,
324
+ cursor: "default",
325
+ },
326
+ },
327
+ stageTools: {
328
+ position: "absolute",
329
+ top: space.xl,
330
+ right: space.xl,
331
+ zIndex: 2,
332
+ display: "flex",
333
+ alignItems: "center",
334
+ gap: space.sm,
335
+ },
336
+ stageTool: {
337
+ width: "32px",
338
+ height: "32px",
339
+ display: "flex",
340
+ alignItems: "center",
341
+ justifyContent: "center",
342
+ padding: 0,
343
+ borderWidth: 1,
344
+ borderStyle: "solid",
345
+ borderColor: "rgba(255, 255, 255, 0.16)",
346
+ borderRadius: "999px",
347
+ backgroundColor: "rgba(0, 0, 0, 0.45)",
348
+ color: "#fafafa",
349
+ cursor: "pointer",
350
+ [hover]: {
351
+ backgroundColor: "rgba(255, 255, 255, 0.16)",
352
+ },
353
+ },
354
+ menu: {
355
+ minWidth: "8rem",
356
+ },
357
+ menuItem: {
358
+ cursor: "pointer",
359
+ },
360
+ menuDanger: {
361
+ color: colors.danger,
362
+ },
363
+ navPrev: {
364
+ left: space.lg,
365
+ },
366
+ navNext: {
367
+ right: space.lg,
368
+ },
369
+ sidebar: {
370
+ width: "320px",
371
+ flexShrink: 0,
372
+ display: "flex",
373
+ flexDirection: "column",
374
+ justifyContent: "flex-end",
375
+ gap: space.xl,
376
+ padding: space.xl,
377
+ backgroundColor: colors.bgSurface,
378
+ color: colors.text,
379
+ borderLeftWidth: 1,
380
+ borderLeftStyle: "solid",
381
+ borderLeftColor: colors.border,
382
+ overflow: "auto",
383
+ [stack]: {
384
+ width: "100%",
385
+ borderLeftWidth: 0,
386
+ borderTopWidth: 1,
387
+ borderTopStyle: "solid",
388
+ borderTopColor: colors.border,
389
+ },
390
+ },
391
+ filename: {
392
+ minWidth: 0,
393
+ overflowWrap: "anywhere",
394
+ },
395
+ facts: {
396
+ display: "flex",
397
+ flexDirection: "column",
398
+ gap: space.lg,
399
+ margin: 0,
400
+ },
401
+ fact: {
402
+ display: "flex",
403
+ flexDirection: "column",
404
+ gap: space.xs,
405
+ },
406
+ factLabel: {
407
+ margin: 0,
408
+ color: colors.textMuted,
409
+ fontSize: type.xs,
410
+ },
411
+ factValue: {
412
+ margin: 0,
413
+ fontSize: type.md,
414
+ fontVariantNumeric: "tabular-nums",
415
+ },
416
+ urlBlock: {
417
+ display: "flex",
418
+ flexDirection: "column",
419
+ alignItems: "stretch",
420
+ gap: space.sm,
421
+ },
422
+ urlHead: {
423
+ display: "flex",
424
+ alignItems: "center",
425
+ justifyContent: "space-between",
426
+ gap: space.md,
427
+ },
428
+ copy: {
429
+ display: "inline-flex",
430
+ alignItems: "center",
431
+ justifyContent: "center",
432
+ gap: space.sm,
433
+ minHeight: "32px",
434
+ paddingInline: space.lg,
435
+ borderWidth: 1,
436
+ borderStyle: "solid",
437
+ borderColor: colors.border,
438
+ borderRadius: radius.md,
439
+ backgroundColor: colors.bgPanel,
440
+ color: colors.text,
441
+ fontFamily: type.sans,
442
+ fontSize: type.sm,
443
+ fontWeight: 500,
444
+ cursor: "pointer",
445
+ [hover]: {
446
+ ":hover": {
447
+ backgroundColor: colors.bgHover,
448
+ },
449
+ },
450
+ },
451
+ url: {
452
+ margin: 0,
453
+ width: "100%",
454
+ fontFamily: type.mono,
455
+ fontSize: type.xs,
456
+ lineHeight: 1.4,
457
+ wordBreak: "break-all",
458
+ color: colors.textMuted,
459
+ },
460
+ });
@@ -2,7 +2,7 @@
2
2
  import { Navigate, Outlet, useLocation, useOutletContext } from "react-router";
3
3
  import * as stylex from "@stylexjs/stylex";
4
4
  import { NavItem } from "../chrome/NavItem";
5
- import { GearIcon, GithubIcon, PlugIcon, SchemaIcon, UsersIcon } from "../chrome/icons";
5
+ import { GearIcon, GithubIcon, PlugIcon, SchemaIcon, UsageIcon, UsersIcon } from "../chrome/icons";
6
6
  import { colors, space, type } from "../styles/tokens.stylex";
7
7
  import type { WorkspaceOutletContext } from "../workspace/types";
8
8
  import { isGithubSettingsPath, settingsPath } from "../workspace-path";
@@ -33,6 +33,12 @@ export function SettingsLayout() {
33
33
  General
34
34
  </NavItem>
35
35
  </li>
36
+ <li>
37
+ <NavItem to={settingsPath(slug, "usage")}>
38
+ <UsageIcon />
39
+ Usage
40
+ </NavItem>
41
+ </li>
36
42
  {develop ? (
37
43
  <li>
38
44
  <NavItem
@@ -0,0 +1,187 @@
1
+ // packages/studio/src/pages/SettingsUsagePage.tsx
2
+ import { useEffect, useState } from "react";
3
+ import { useOutletContext } from "react-router";
4
+ import { fetchWorkspaceUsage, type WorkspaceUsage } from "../api";
5
+ import { PageHeader, Panel, Stack } from "../ui/Page";
6
+ import { Text } from "../ui/Text";
7
+ import * as stylex from "@stylexjs/stylex";
8
+ import { colors, space, type } from "../styles/tokens.stylex";
9
+ import type { WorkspaceOutletContext } from "../workspace/types";
10
+
11
+ const SURFACES: { key: keyof WorkspaceUsage["dataRequests"]; label: string }[] = [
12
+ { key: "public", label: "Public" },
13
+ { key: "studio", label: "Studio" },
14
+ { key: "replica", label: "Replica" },
15
+ { key: "preview", label: "Preview" },
16
+ { key: "ai", label: "Assistant" },
17
+ ];
18
+
19
+ const WORK: { key: keyof WorkspaceUsage["work"]; label: string }[] = [
20
+ { key: "commits", label: "Commits" },
21
+ { key: "commitFiles", label: "Files committed" },
22
+ { key: "imports", label: "Imports" },
23
+ { key: "importFiles", label: "Files imported" },
24
+ { key: "saves", label: "Saves" },
25
+ { key: "mediaTransforms", label: "Image transforms" },
26
+ ];
27
+
28
+ function formatBytes(value: number): string {
29
+ if (value < 1024) return `${value} B`;
30
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
31
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MB`;
32
+ return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
33
+ }
34
+
35
+ function formatCount(value: number): string {
36
+ return new Intl.NumberFormat("en-US").format(value);
37
+ }
38
+
39
+ function monthLabel(period: string): string {
40
+ const [year, month] = period.split("-").map(Number);
41
+ if (!year || !month) return period;
42
+ return new Intl.DateTimeFormat("en-US", {
43
+ month: "long",
44
+ year: "numeric",
45
+ timeZone: "UTC",
46
+ }).format(new Date(Date.UTC(year, month - 1, 1)));
47
+ }
48
+
49
+ export function SettingsUsagePage() {
50
+ const { workspace } = useOutletContext<WorkspaceOutletContext>();
51
+ const [usage, setUsage] = useState<WorkspaceUsage | null>(null);
52
+ const [error, setError] = useState<string | null>(null);
53
+
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+ setUsage(null);
57
+ setError(null);
58
+ fetchWorkspaceUsage(workspace.id)
59
+ .then((body) => {
60
+ if (!cancelled) setUsage(body);
61
+ })
62
+ .catch((err: unknown) => {
63
+ if (!cancelled) setError(err instanceof Error ? err.message : "Could not load usage");
64
+ });
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, [workspace.id]);
69
+
70
+ const total = usage
71
+ ? SURFACES.reduce((sum, surface) => sum + usage.dataRequests[surface.key], 0)
72
+ : 0;
73
+
74
+ return (
75
+ <Stack gap="lg">
76
+ <PageHeader
77
+ title="Usage"
78
+ description={
79
+ usage
80
+ ? `${monthLabel(usage.period)}, UTC. Counts update within about a minute.`
81
+ : "Content reads and AI tokens for this workspace."
82
+ }
83
+ />
84
+ {error ? <Text tone="danger">{error}</Text> : null}
85
+ {!usage && !error ? <Text tone="muted">Loading…</Text> : null}
86
+ {usage ? (
87
+ <>
88
+ <Panel>
89
+ <Text as="h2" size="lg" weight="medium">
90
+ Data requests
91
+ </Text>
92
+ <dl {...stylex.props(styles.list)}>
93
+ <div {...stylex.props(styles.row)}>
94
+ <dt {...stylex.props(styles.label)}>Total</dt>
95
+ <dd {...stylex.props(styles.value)}>{formatCount(total)}</dd>
96
+ </div>
97
+ {SURFACES.map((surface) => (
98
+ <div key={surface.key} {...stylex.props(styles.row)}>
99
+ <dt {...stylex.props(styles.label)}>{surface.label}</dt>
100
+ <dd {...stylex.props(styles.value)}>
101
+ {formatCount(usage.dataRequests[surface.key])}
102
+ </dd>
103
+ </div>
104
+ ))}
105
+ </dl>
106
+ </Panel>
107
+ <Panel>
108
+ <Text as="h2" size="lg" weight="medium">
109
+ Work
110
+ </Text>
111
+ <dl {...stylex.props(styles.list)}>
112
+ {WORK.map((item) => (
113
+ <div key={item.key} {...stylex.props(styles.row)}>
114
+ <dt {...stylex.props(styles.label)}>{item.label}</dt>
115
+ <dd {...stylex.props(styles.value)}>{formatCount(usage.work[item.key])}</dd>
116
+ </div>
117
+ ))}
118
+ </dl>
119
+ </Panel>
120
+ <Panel>
121
+ <Text as="h2" size="lg" weight="medium">
122
+ Media
123
+ </Text>
124
+ <dl {...stylex.props(styles.list)}>
125
+ <div {...stylex.props(styles.row)}>
126
+ <dt {...stylex.props(styles.label)}>Stored</dt>
127
+ <dd {...stylex.props(styles.value)}>{formatBytes(usage.mediaBytes)}</dd>
128
+ </div>
129
+ </dl>
130
+ </Panel>
131
+ <Panel>
132
+ <Text as="h2" size="lg" weight="medium">
133
+ AI
134
+ </Text>
135
+ <dl {...stylex.props(styles.list)}>
136
+ <div {...stylex.props(styles.row)}>
137
+ <dt {...stylex.props(styles.label)}>Input tokens</dt>
138
+ <dd {...stylex.props(styles.value)}>{formatCount(usage.ai.inputTokens)}</dd>
139
+ </div>
140
+ <div {...stylex.props(styles.row)}>
141
+ <dt {...stylex.props(styles.label)}>Output tokens</dt>
142
+ <dd {...stylex.props(styles.value)}>{formatCount(usage.ai.outputTokens)}</dd>
143
+ </div>
144
+ </dl>
145
+ </Panel>
146
+ <Panel>
147
+ <div {...stylex.props(styles.row)}>
148
+ <Text as="h2" size="lg" weight="medium">
149
+ Members
150
+ </Text>
151
+ <Text size="lg" weight="medium">
152
+ {formatCount(usage.members)}
153
+ </Text>
154
+ </div>
155
+ </Panel>
156
+ </>
157
+ ) : null}
158
+ </Stack>
159
+ );
160
+ }
161
+
162
+ const styles = stylex.create({
163
+ list: {
164
+ margin: 0,
165
+ marginTop: space.lg,
166
+ display: "flex",
167
+ flexDirection: "column",
168
+ gap: space.sm,
169
+ },
170
+ row: {
171
+ display: "flex",
172
+ alignItems: "baseline",
173
+ justifyContent: "space-between",
174
+ gap: space.lg,
175
+ },
176
+ label: {
177
+ margin: 0,
178
+ fontSize: type.sm,
179
+ color: colors.textMuted,
180
+ },
181
+ value: {
182
+ margin: 0,
183
+ fontSize: type.sm,
184
+ fontVariantNumeric: "tabular-nums",
185
+ color: colors.text,
186
+ },
187
+ });