@backmatter/studio 0.0.4 → 0.0.6
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.
- package/dist/chunks/{EmbedPublishPage-hx1xoAhI.js → EmbedPublishPage-DazYJLaD.js} +13 -13
- package/dist/chunks/{UiKitPage-BOunq6mr.js → UiKitPage-DyVyBp0I.js} +9 -9
- package/dist/chunks/{WorkspaceRoutes-CTFTWu3L.js → WorkspaceRoutes-CPvkgvxe.js} +10869 -10957
- package/dist/chunks/{dashboard-upmiawx5.js → dashboard-6aSS8oyj.js} +194 -194
- package/dist/chunks/{index-CBvevwfT.js → index-DgTNL4t3.js} +2 -2
- package/dist/chunks/{status-bar-action-DiXhfcj3.js → status-bar-action-RNBujSgk.js} +5655 -5427
- package/dist/dashboard.js +2 -2
- package/dist/index.js +18 -18
- package/dist/site-widget.js +790 -234
- package/dist/studio.css +4 -4
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +2 -0
- package/src/api.ts +24 -0
- package/src/chat/ChatPanel.tsx +1 -1
- package/src/chrome/icons.tsx +13 -0
- package/src/pages/SettingsLayout.tsx +7 -1
- package/src/pages/SettingsUsagePage.tsx +168 -0
- package/src/preview-frame.tsx +78 -0
- package/src/site-bar.tsx +769 -0
- package/src/site-widget.tsx +38 -163
package/dist/studio.css
CHANGED
|
@@ -1455,14 +1455,14 @@
|
|
|
1455
1455
|
bottom: 44px;
|
|
1456
1456
|
}
|
|
1457
1457
|
|
|
1458
|
-
.xy75b87 {
|
|
1459
|
-
bottom: 64px;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
1458
|
.x1bs4c7o {
|
|
1463
1459
|
bottom: calc(100% - 12px);
|
|
1464
1460
|
}
|
|
1465
1461
|
|
|
1462
|
+
.xpfn2fm {
|
|
1463
|
+
bottom: calc(48px + env(safe-area-inset-bottom, 0px));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
1466
|
.x1sf7pbd {
|
|
1467
1467
|
bottom: var(--xvc7yhn);
|
|
1468
1468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "The Backmatter editing surface: document editor, AI chat, publish. Mounted on your site or hosted on the dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@ai-sdk/react": "^4.0.100",
|
|
55
|
-
"@backmatter/core": "^0.0.
|
|
55
|
+
"@backmatter/core": "^0.0.22",
|
|
56
56
|
"@base-ui/react": "^1.6.0",
|
|
57
57
|
"@dnd-kit/abstract": "^0.5.0",
|
|
58
58
|
"@dnd-kit/react": "^0.5.0",
|
package/src/WorkspaceRoutes.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { WorkspaceLayout } from "./pages/WorkspaceLayout";
|
|
|
6
6
|
import { WorkspaceHomeRedirect } from "./pages/WorkspaceHomeRedirect";
|
|
7
7
|
import { TypeDocsPage } from "./pages/TypeDocsPage";
|
|
8
8
|
import { SettingsGeneralPage } from "./pages/SettingsGeneralPage";
|
|
9
|
+
import { SettingsUsagePage } from "./pages/SettingsUsagePage";
|
|
9
10
|
import { SettingsGithubPage } from "./pages/SettingsGithubPage";
|
|
10
11
|
import { SettingsIndexRedirect, SettingsLayout } from "./pages/SettingsLayout";
|
|
11
12
|
import { SettingsSchemaPage } from "./pages/SettingsSchemaPage";
|
|
@@ -26,6 +27,7 @@ export function workspaceRoutes() {
|
|
|
26
27
|
<Route element={<SettingsLayout />}>
|
|
27
28
|
<Route path="settings" element={<SettingsIndexRedirect />} />
|
|
28
29
|
<Route path="settings/general" element={<SettingsGeneralPage />} />
|
|
30
|
+
<Route path="settings/usage" element={<SettingsUsagePage />} />
|
|
29
31
|
<Route path="settings/github" element={<SettingsGithubPage />} />
|
|
30
32
|
<Route path="settings/schema" element={<SettingsSchemaPage />} />
|
|
31
33
|
<Route path="settings/mcp" element={<SettingsMcpPage />} />
|
package/src/api.ts
CHANGED
|
@@ -169,10 +169,34 @@ export type WorkspaceDetail = {
|
|
|
169
169
|
hasHostToken: boolean;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
export type WorkspaceUsage = {
|
|
173
|
+
period: string;
|
|
174
|
+
dataRequests: {
|
|
175
|
+
public: number;
|
|
176
|
+
studio: number;
|
|
177
|
+
replica: number;
|
|
178
|
+
preview: number;
|
|
179
|
+
ai: number;
|
|
180
|
+
};
|
|
181
|
+
work: {
|
|
182
|
+
commits: number;
|
|
183
|
+
commitFiles: number;
|
|
184
|
+
imports: number;
|
|
185
|
+
importFiles: number;
|
|
186
|
+
saves: number;
|
|
187
|
+
};
|
|
188
|
+
ai: { inputTokens: number; outputTokens: number };
|
|
189
|
+
members: number;
|
|
190
|
+
};
|
|
191
|
+
|
|
172
192
|
export function fetchWorkspace(workspaceId: string) {
|
|
173
193
|
return api<{ workspace: WorkspaceDetail }>(`/workspaces/${workspaceId}`);
|
|
174
194
|
}
|
|
175
195
|
|
|
196
|
+
export function fetchWorkspaceUsage(workspaceId: string) {
|
|
197
|
+
return api<WorkspaceUsage>(`/workspaces/${workspaceId}/usage`);
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
export function fetchReplica(workspaceId: string, cache?: ReplicaCache) {
|
|
177
201
|
const query = cache ? `?${replicaQuery(cache)}` : "";
|
|
178
202
|
return api<ReplicaBody>(`/workspaces/${workspaceId}/replica${query}`);
|
package/src/chat/ChatPanel.tsx
CHANGED
package/src/chrome/icons.tsx
CHANGED
|
@@ -20,6 +20,19 @@ export function DocIcon() {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function UsageIcon() {
|
|
24
|
+
return (
|
|
25
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
26
|
+
<path
|
|
27
|
+
d="M3 12.5V8.5M8 12.5V3.5M13 12.5V6"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="1.25"
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
/>
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export function GearIcon() {
|
|
24
37
|
return (
|
|
25
38
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
@@ -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,168 @@
|
|
|
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
|
+
];
|
|
26
|
+
|
|
27
|
+
function formatCount(value: number): string {
|
|
28
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function monthLabel(period: string): string {
|
|
32
|
+
const [year, month] = period.split("-").map(Number);
|
|
33
|
+
if (!year || !month) return period;
|
|
34
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
35
|
+
month: "long",
|
|
36
|
+
year: "numeric",
|
|
37
|
+
timeZone: "UTC",
|
|
38
|
+
}).format(new Date(Date.UTC(year, month - 1, 1)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function SettingsUsagePage() {
|
|
42
|
+
const { workspace } = useOutletContext<WorkspaceOutletContext>();
|
|
43
|
+
const [usage, setUsage] = useState<WorkspaceUsage | null>(null);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
setUsage(null);
|
|
49
|
+
setError(null);
|
|
50
|
+
fetchWorkspaceUsage(workspace.id)
|
|
51
|
+
.then((body) => {
|
|
52
|
+
if (!cancelled) setUsage(body);
|
|
53
|
+
})
|
|
54
|
+
.catch((err: unknown) => {
|
|
55
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Could not load usage");
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
};
|
|
60
|
+
}, [workspace.id]);
|
|
61
|
+
|
|
62
|
+
const total = usage
|
|
63
|
+
? SURFACES.reduce((sum, surface) => sum + usage.dataRequests[surface.key], 0)
|
|
64
|
+
: 0;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Stack gap="lg">
|
|
68
|
+
<PageHeader
|
|
69
|
+
title="Usage"
|
|
70
|
+
description={
|
|
71
|
+
usage
|
|
72
|
+
? `${monthLabel(usage.period)}, UTC. Counts update within about a minute.`
|
|
73
|
+
: "Content reads and AI tokens for this workspace."
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
{error ? <Text tone="danger">{error}</Text> : null}
|
|
77
|
+
{!usage && !error ? <Text tone="muted">Loading…</Text> : null}
|
|
78
|
+
{usage ? (
|
|
79
|
+
<>
|
|
80
|
+
<Panel>
|
|
81
|
+
<Text as="h2" size="lg" weight="medium">
|
|
82
|
+
Data requests
|
|
83
|
+
</Text>
|
|
84
|
+
<dl {...stylex.props(styles.list)}>
|
|
85
|
+
<div {...stylex.props(styles.row)}>
|
|
86
|
+
<dt {...stylex.props(styles.label)}>Total</dt>
|
|
87
|
+
<dd {...stylex.props(styles.value)}>{formatCount(total)}</dd>
|
|
88
|
+
</div>
|
|
89
|
+
{SURFACES.map((surface) => (
|
|
90
|
+
<div key={surface.key} {...stylex.props(styles.row)}>
|
|
91
|
+
<dt {...stylex.props(styles.label)}>{surface.label}</dt>
|
|
92
|
+
<dd {...stylex.props(styles.value)}>
|
|
93
|
+
{formatCount(usage.dataRequests[surface.key])}
|
|
94
|
+
</dd>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</dl>
|
|
98
|
+
</Panel>
|
|
99
|
+
<Panel>
|
|
100
|
+
<Text as="h2" size="lg" weight="medium">
|
|
101
|
+
Work
|
|
102
|
+
</Text>
|
|
103
|
+
<dl {...stylex.props(styles.list)}>
|
|
104
|
+
{WORK.map((item) => (
|
|
105
|
+
<div key={item.key} {...stylex.props(styles.row)}>
|
|
106
|
+
<dt {...stylex.props(styles.label)}>{item.label}</dt>
|
|
107
|
+
<dd {...stylex.props(styles.value)}>{formatCount(usage.work[item.key])}</dd>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</dl>
|
|
111
|
+
</Panel>
|
|
112
|
+
<Panel>
|
|
113
|
+
<Text as="h2" size="lg" weight="medium">
|
|
114
|
+
AI
|
|
115
|
+
</Text>
|
|
116
|
+
<dl {...stylex.props(styles.list)}>
|
|
117
|
+
<div {...stylex.props(styles.row)}>
|
|
118
|
+
<dt {...stylex.props(styles.label)}>Input tokens</dt>
|
|
119
|
+
<dd {...stylex.props(styles.value)}>{formatCount(usage.ai.inputTokens)}</dd>
|
|
120
|
+
</div>
|
|
121
|
+
<div {...stylex.props(styles.row)}>
|
|
122
|
+
<dt {...stylex.props(styles.label)}>Output tokens</dt>
|
|
123
|
+
<dd {...stylex.props(styles.value)}>{formatCount(usage.ai.outputTokens)}</dd>
|
|
124
|
+
</div>
|
|
125
|
+
</dl>
|
|
126
|
+
</Panel>
|
|
127
|
+
<Panel>
|
|
128
|
+
<div {...stylex.props(styles.row)}>
|
|
129
|
+
<Text as="h2" size="lg" weight="medium">
|
|
130
|
+
Members
|
|
131
|
+
</Text>
|
|
132
|
+
<Text size="lg" weight="medium">
|
|
133
|
+
{formatCount(usage.members)}
|
|
134
|
+
</Text>
|
|
135
|
+
</div>
|
|
136
|
+
</Panel>
|
|
137
|
+
</>
|
|
138
|
+
) : null}
|
|
139
|
+
</Stack>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const styles = stylex.create({
|
|
144
|
+
list: {
|
|
145
|
+
margin: 0,
|
|
146
|
+
marginTop: space.lg,
|
|
147
|
+
display: "flex",
|
|
148
|
+
flexDirection: "column",
|
|
149
|
+
gap: space.sm,
|
|
150
|
+
},
|
|
151
|
+
row: {
|
|
152
|
+
display: "flex",
|
|
153
|
+
alignItems: "baseline",
|
|
154
|
+
justifyContent: "space-between",
|
|
155
|
+
gap: space.lg,
|
|
156
|
+
},
|
|
157
|
+
label: {
|
|
158
|
+
margin: 0,
|
|
159
|
+
fontSize: type.sm,
|
|
160
|
+
color: colors.textMuted,
|
|
161
|
+
},
|
|
162
|
+
value: {
|
|
163
|
+
margin: 0,
|
|
164
|
+
fontSize: type.sm,
|
|
165
|
+
fontVariantNumeric: "tabular-nums",
|
|
166
|
+
color: colors.text,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// packages/studio/src/preview-frame.tsx
|
|
2
|
+
// Full-viewport ring and a top-right preview label while the site session is active.
|
|
3
|
+
export const PREVIEW_FRAME_CLASS = "backmatter-preview-frame";
|
|
4
|
+
|
|
5
|
+
/** Shared with the on-site dev bar while preview is active. */
|
|
6
|
+
export const PREVIEW_CHROME_BORDER = "#d97706";
|
|
7
|
+
|
|
8
|
+
function EyeIcon() {
|
|
9
|
+
return (
|
|
10
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
11
|
+
<path
|
|
12
|
+
d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
strokeWidth="1.25"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
/>
|
|
17
|
+
<circle cx="8" cy="8" r="1.75" stroke="currentColor" strokeWidth="1.25" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function PreviewFrame() {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<style>{FRAME_CSS}</style>
|
|
26
|
+
<div
|
|
27
|
+
className={PREVIEW_FRAME_CLASS}
|
|
28
|
+
data-backmatter-preview-frame=""
|
|
29
|
+
aria-hidden
|
|
30
|
+
>
|
|
31
|
+
<div className={`${PREVIEW_FRAME_CLASS}-badge`}>
|
|
32
|
+
<EyeIcon />
|
|
33
|
+
<span>Preview</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FRAME_CSS = `
|
|
41
|
+
.${PREVIEW_FRAME_CLASS} {
|
|
42
|
+
position: fixed;
|
|
43
|
+
inset: 0;
|
|
44
|
+
z-index: 2147482999;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
box-shadow: inset 0 0 0 2px ${PREVIEW_CHROME_BORDER};
|
|
47
|
+
overflow: visible;
|
|
48
|
+
}
|
|
49
|
+
.${PREVIEW_FRAME_CLASS}-badge {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: env(safe-area-inset-top, 0px);
|
|
52
|
+
right: env(safe-area-inset-right, 0px);
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 6px 12px;
|
|
57
|
+
border: 2px solid ${PREVIEW_CHROME_BORDER};
|
|
58
|
+
border-top: 0;
|
|
59
|
+
border-radius: 0 0 14px 14px;
|
|
60
|
+
background: #fff7ed;
|
|
61
|
+
color: #9a3412;
|
|
62
|
+
font: 500 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
-webkit-font-smoothing: antialiased;
|
|
65
|
+
}
|
|
66
|
+
@keyframes backmatter-preview-frame-in {
|
|
67
|
+
from { opacity: 0; }
|
|
68
|
+
to { opacity: 1; }
|
|
69
|
+
}
|
|
70
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
71
|
+
.${PREVIEW_FRAME_CLASS} {
|
|
72
|
+
animation: backmatter-preview-frame-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
@media (prefers-reduced-motion: reduce) {
|
|
76
|
+
.${PREVIEW_FRAME_CLASS} { animation: none; }
|
|
77
|
+
}
|
|
78
|
+
`;
|