@backmatter/studio 0.0.15 → 0.0.16
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/{UiKitApp-BDsrXN12.js → UiKitApp-_EDqWID3.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-B2PrFgpz.js} +28979 -27418
- package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-BnlnMnnr.js} +55 -54
- package/dist/chunks/{index-DHJvh5Uy.js → index-D_BHF_8q.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +2 -2
- package/dist/site-widget.css +4 -0
- package/dist/site-widget.js +8304 -8029
- package/dist/studio.css +186 -12
- package/package.json +2 -2
- package/src/WorkspaceRoutes.tsx +5 -2
- package/src/api.ts +63 -7
- package/src/chat/ChangeCard.tsx +8 -2
- package/src/chat/ChatMessage.tsx +171 -102
- package/src/chat/ChatPanel.tsx +54 -34
- package/src/chat/ChatProvider.tsx +116 -20
- package/src/chat/Composer.tsx +48 -7
- package/src/chat/PointerChip.tsx +9 -5
- package/src/chat/ThreadHistory.tsx +3 -3
- package/src/chat/chat-context.ts +67 -3
- package/src/chat/chat-helpers.ts +12 -0
- package/src/chat/chat-state.ts +11 -0
- package/src/chrome/AppShell.tsx +16 -5
- package/src/chrome/CommandPalette.tsx +12 -0
- package/src/chrome/ContentExplorerSidebar.tsx +23 -3
- package/src/chrome/UserMenu.tsx +34 -3
- package/src/chrome/WorkspaceStatusBar.tsx +209 -27
- package/src/chrome/content-api-url.ts +0 -78
- package/src/chrome/content-tree-visibility.ts +13 -3
- package/src/chrome/explorer-icons.tsx +5 -0
- package/src/code-session-entry.ts +9 -0
- package/src/components/WorkspaceSwitcher.tsx +2 -2
- package/src/element-origin.ts +149 -0
- package/src/host-pointer-bootstrap.ts +11 -0
- package/src/host-pointer.ts +147 -0
- package/src/install-preview-pointer.ts +19 -3
- package/src/pages/CodeSessionFiles.tsx +221 -0
- package/src/pages/CodeSessionPage.tsx +1188 -0
- package/src/pages/DocEditorPage.tsx +33 -15
- package/src/pages/PreviewSize.tsx +181 -0
- package/src/pages/SettingsGeneralPage.tsx +67 -2
- package/src/pages/WorkspaceHomePage.tsx +313 -0
- package/src/pages/WorkspaceLayout.tsx +10 -1
- package/src/pages/code-session-boot.ts +43 -0
- package/src/pages/code-session-context.tsx +51 -0
- package/src/pages/code-session-files.ts +42 -0
- package/src/pages/code-session-frame.ts +52 -0
- package/src/pages/code-session-routes.ts +42 -0
- package/src/pages/home-rows.ts +140 -0
- package/src/pages/preview-size.ts +33 -0
- package/src/pages/session-preview.ts +109 -0
- package/src/pages/use-session-preview.ts +70 -0
- package/src/replica/store.ts +11 -0
- package/src/replica/types.ts +6 -0
- package/src/site-pointer.ts +24 -5
- package/src/studio-config.ts +1 -1
- package/src/ui/ListRow.tsx +12 -3
- package/src/ui/Page.tsx +12 -1
- package/src/workspace-path.ts +35 -3
- package/src/chrome/ContentApiPopover.tsx +0 -215
- package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
persistPreviewPct,
|
|
47
47
|
readPreviewPct,
|
|
48
48
|
} from "./preview-split";
|
|
49
|
+
import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
|
|
50
|
+
import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
|
|
49
51
|
import {
|
|
50
52
|
editorPreviewPath,
|
|
51
53
|
modelForType,
|
|
@@ -214,6 +216,7 @@ export function DocEditorPage() {
|
|
|
214
216
|
const [previewOffer, setPreviewOffer] = useState<PreviewChannelRef | null>(null);
|
|
215
217
|
const [previewLive, setPreviewLive] = useState<"rendered" | "error" | null>(null);
|
|
216
218
|
const [previewPct, setPreviewPct] = useState(readPreviewPct);
|
|
219
|
+
const [previewSize, setPreviewSize] = useState(readPreviewSize);
|
|
217
220
|
const [dragging, setDragging] = useState(false);
|
|
218
221
|
const [editEpoch, setEditEpoch] = useState(0);
|
|
219
222
|
const [selection, setSelection] = useState<EditorSelection | null>(null);
|
|
@@ -323,6 +326,7 @@ export function DocEditorPage() {
|
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
function onPreviewGrab(grab: PreviewGrabMessage) {
|
|
329
|
+
if (typeof grab.blockId !== "string" || !grab.blockId) return;
|
|
326
330
|
const ids = blocksFromFrontmatter(latest.current.frontmatter[blocksFieldRef.current]).map(
|
|
327
331
|
(block) => block.id,
|
|
328
332
|
);
|
|
@@ -330,7 +334,13 @@ export function DocEditorPage() {
|
|
|
330
334
|
grabRef: grab.ref,
|
|
331
335
|
openRef: channelRef.current,
|
|
332
336
|
blockIds: ids,
|
|
333
|
-
grab
|
|
337
|
+
grab: {
|
|
338
|
+
blockId: grab.blockId,
|
|
339
|
+
blockType: grab.blockType,
|
|
340
|
+
blockIndex: grab.blockIndex,
|
|
341
|
+
quote: grab.quote,
|
|
342
|
+
shift: grab.shift,
|
|
343
|
+
},
|
|
334
344
|
});
|
|
335
345
|
if (decision.action === "add") {
|
|
336
346
|
addPointer({
|
|
@@ -467,6 +477,11 @@ export function DocEditorPage() {
|
|
|
467
477
|
setPreviewKey((k) => k + 1);
|
|
468
478
|
}
|
|
469
479
|
|
|
480
|
+
function choosePreviewSize(size: PreviewSize) {
|
|
481
|
+
setPreviewSize(size);
|
|
482
|
+
persistPreviewSize(size);
|
|
483
|
+
}
|
|
484
|
+
|
|
470
485
|
function updatePreviewPctFromPointer(clientX: number, clientY: number) {
|
|
471
486
|
const split = splitRef.current;
|
|
472
487
|
if (!split) return;
|
|
@@ -901,6 +916,7 @@ export function DocEditorPage() {
|
|
|
901
916
|
</button>
|
|
902
917
|
) : null}
|
|
903
918
|
</div>
|
|
919
|
+
<PreviewSizeSwitch size={previewSize} onSize={choosePreviewSize} />
|
|
904
920
|
<span
|
|
905
921
|
{...stylex.props(
|
|
906
922
|
styles.badge,
|
|
@@ -911,20 +927,22 @@ export function DocEditorPage() {
|
|
|
911
927
|
</span>
|
|
912
928
|
</div>
|
|
913
929
|
{previewUrl ? (
|
|
914
|
-
<
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
930
|
+
<PreviewViewport size={previewSize} dragging={dragging}>
|
|
931
|
+
<iframe
|
|
932
|
+
key={`${previewKey}-${previewUrl}`}
|
|
933
|
+
ref={previewFrameRef}
|
|
934
|
+
title="Live preview"
|
|
935
|
+
src={previewUrl}
|
|
936
|
+
onLoad={() => {
|
|
937
|
+
setPreviewLoading(false);
|
|
938
|
+
setPreviewFrame((n) => n + 1);
|
|
939
|
+
}}
|
|
940
|
+
{...stylex.props(
|
|
941
|
+
styles.iframe,
|
|
942
|
+
dragging && styles.iframeDragging,
|
|
943
|
+
)}
|
|
944
|
+
/>
|
|
945
|
+
</PreviewViewport>
|
|
928
946
|
) : !siteUrl ? (
|
|
929
947
|
<div {...stylex.props(styles.iframe, styles.previewEmpty)}>
|
|
930
948
|
<Text as="p" size="sm" weight="medium">
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// packages/studio/src/pages/PreviewSize.tsx
|
|
2
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
|
+
import * as stylex from "@stylexjs/stylex";
|
|
4
|
+
import {
|
|
5
|
+
IconImac,
|
|
6
|
+
IconPhone,
|
|
7
|
+
IconTablet,
|
|
8
|
+
} from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
|
|
9
|
+
import { colors, radius } from "../styles/tokens.stylex";
|
|
10
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
11
|
+
import {
|
|
12
|
+
previewFrameWidth,
|
|
13
|
+
type PreviewSize,
|
|
14
|
+
} from "./preview-size";
|
|
15
|
+
|
|
16
|
+
const OPTIONS: readonly { size: PreviewSize; label: string; icon: typeof IconImac }[] = [
|
|
17
|
+
{ size: "desktop", label: "Desktop", icon: IconImac },
|
|
18
|
+
{ size: "tablet", label: "Tablet", icon: IconTablet },
|
|
19
|
+
{ size: "phone", label: "Phone", icon: IconPhone },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function PreviewSizeSwitch({
|
|
23
|
+
size,
|
|
24
|
+
onSize,
|
|
25
|
+
disabled = false,
|
|
26
|
+
}: {
|
|
27
|
+
size: PreviewSize;
|
|
28
|
+
onSize: (size: PreviewSize) => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
32
|
+
if (disabled) return;
|
|
33
|
+
const index = OPTIONS.findIndex((option) => option.size === size);
|
|
34
|
+
const next =
|
|
35
|
+
event.key === "ArrowRight" ? OPTIONS[(index + 1) % OPTIONS.length]
|
|
36
|
+
: event.key === "ArrowLeft" ? OPTIONS[(index - 1 + OPTIONS.length) % OPTIONS.length]
|
|
37
|
+
: null;
|
|
38
|
+
if (!next) return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
onSize(next.size);
|
|
41
|
+
event.currentTarget.querySelector<HTMLButtonElement>(`[data-preview-size="${next.size}"]`)?.focus();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
role="radiogroup"
|
|
47
|
+
aria-label="Preview size"
|
|
48
|
+
onKeyDown={onKeyDown}
|
|
49
|
+
{...stylex.props(styles.group)}
|
|
50
|
+
>
|
|
51
|
+
{OPTIONS.map((option) => {
|
|
52
|
+
const selected = option.size === size;
|
|
53
|
+
const Icon = option.icon;
|
|
54
|
+
return (
|
|
55
|
+
<Tooltip key={option.size} label={option.label}>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
role="radio"
|
|
59
|
+
data-preview-size={option.size}
|
|
60
|
+
aria-label={option.label}
|
|
61
|
+
aria-checked={selected}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
tabIndex={selected ? 0 : -1}
|
|
64
|
+
onClick={() => onSize(option.size)}
|
|
65
|
+
{...stylex.props(
|
|
66
|
+
styles.option,
|
|
67
|
+
selected && styles.optionOn,
|
|
68
|
+
disabled && styles.optionDisabled,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<Icon size={14} ariaHidden />
|
|
72
|
+
</button>
|
|
73
|
+
</Tooltip>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Centers a fixed-width preview and scrolls sideways when the pane is narrower. */
|
|
81
|
+
export function PreviewViewport({
|
|
82
|
+
size,
|
|
83
|
+
dragging = false,
|
|
84
|
+
children,
|
|
85
|
+
}: {
|
|
86
|
+
size: PreviewSize;
|
|
87
|
+
dragging?: boolean;
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
}) {
|
|
90
|
+
const width = previewFrameWidth(size);
|
|
91
|
+
const viewport = stylex.props(styles.viewport, dragging && styles.dragging);
|
|
92
|
+
const slot = stylex.props(styles.slot, width !== null && styles.slotFixed);
|
|
93
|
+
return (
|
|
94
|
+
<div className={viewport.className} style={viewport.style}>
|
|
95
|
+
<div
|
|
96
|
+
className={slot.className}
|
|
97
|
+
style={width === null ? slot.style : { ...slot.style, width }}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hover = "@media (hover: hover)";
|
|
106
|
+
|
|
107
|
+
const styles = stylex.create({
|
|
108
|
+
group: {
|
|
109
|
+
display: "inline-flex",
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
gap: "2px",
|
|
112
|
+
padding: "2px",
|
|
113
|
+
borderRadius: radius.md,
|
|
114
|
+
backgroundColor: colors.bgPanelInset,
|
|
115
|
+
},
|
|
116
|
+
option: {
|
|
117
|
+
display: "inline-flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
width: "28px",
|
|
121
|
+
height: "22px",
|
|
122
|
+
padding: 0,
|
|
123
|
+
borderWidth: 0,
|
|
124
|
+
borderRadius: radius.sm,
|
|
125
|
+
color: colors.textMuted,
|
|
126
|
+
backgroundColor: "transparent",
|
|
127
|
+
cursor: "pointer",
|
|
128
|
+
[hover]: {
|
|
129
|
+
":hover:not(:disabled)": {
|
|
130
|
+
color: colors.text,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
":focus-visible": {
|
|
134
|
+
outlineWidth: 2,
|
|
135
|
+
outlineStyle: "solid",
|
|
136
|
+
outlineColor: colors.focus,
|
|
137
|
+
outlineOffset: 1,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
optionOn: {
|
|
141
|
+
color: colors.text,
|
|
142
|
+
backgroundColor: colors.bgSurface,
|
|
143
|
+
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
|
144
|
+
},
|
|
145
|
+
optionDisabled: {
|
|
146
|
+
cursor: "default",
|
|
147
|
+
color: colors.border,
|
|
148
|
+
},
|
|
149
|
+
viewport: {
|
|
150
|
+
display: "flex",
|
|
151
|
+
flex: 1,
|
|
152
|
+
alignItems: "stretch",
|
|
153
|
+
justifyContent: "center",
|
|
154
|
+
minWidth: 0,
|
|
155
|
+
minHeight: 0,
|
|
156
|
+
overflowX: "auto",
|
|
157
|
+
overflowY: "hidden",
|
|
158
|
+
},
|
|
159
|
+
dragging: {
|
|
160
|
+
pointerEvents: "none",
|
|
161
|
+
},
|
|
162
|
+
slot: {
|
|
163
|
+
position: "relative",
|
|
164
|
+
display: "flex",
|
|
165
|
+
flexDirection: "column",
|
|
166
|
+
flexGrow: 1,
|
|
167
|
+
flexShrink: 1,
|
|
168
|
+
flexBasis: "0%",
|
|
169
|
+
minWidth: 0,
|
|
170
|
+
minHeight: 0,
|
|
171
|
+
backgroundColor: colors.bgSurface,
|
|
172
|
+
},
|
|
173
|
+
slotFixed: {
|
|
174
|
+
flexGrow: 0,
|
|
175
|
+
flexShrink: 0,
|
|
176
|
+
flexBasis: "auto",
|
|
177
|
+
outlineWidth: 1,
|
|
178
|
+
outlineStyle: "solid",
|
|
179
|
+
outlineColor: colors.border,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
rotateHostToken,
|
|
8
8
|
rotatePreviewToken,
|
|
9
9
|
} from "../api";
|
|
10
|
+
import { publicContentApiUrl } from "../chrome/content-api-url";
|
|
11
|
+
import { useReplicaState } from "../replica/useReplica";
|
|
10
12
|
import { hostEnvSnippet } from "./host-env";
|
|
11
13
|
import { isStudioDev, publicApiUrl } from "../studio-config";
|
|
12
14
|
import { revalidateWebhookUrl } from "./preview-url";
|
|
@@ -24,6 +26,7 @@ export function SettingsGeneralPage() {
|
|
|
24
26
|
const [previewTokenOnce, setPreviewTokenOnce] = useState<string | null>(null);
|
|
25
27
|
const [hostTokenOnce, setHostTokenOnce] = useState<string | null>(null);
|
|
26
28
|
const [copiedId, setCopiedId] = useState(false);
|
|
29
|
+
const [copiedApi, setCopiedApi] = useState(false);
|
|
27
30
|
const [copiedEnv, setCopiedEnv] = useState(false);
|
|
28
31
|
const [siteUrl, setSiteUrl] = useState(workspace.siteUrls?.[0] ?? "");
|
|
29
32
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -34,13 +37,25 @@ export function SettingsGeneralPage() {
|
|
|
34
37
|
const webhookUrl = revalidateWebhookUrl(siteUrl);
|
|
35
38
|
|
|
36
39
|
useEffect(() => {
|
|
37
|
-
if (!copiedId && !copiedEnv) return;
|
|
40
|
+
if (!copiedId && !copiedApi && !copiedEnv) return;
|
|
38
41
|
const id = window.setTimeout(() => {
|
|
39
42
|
setCopiedId(false);
|
|
43
|
+
setCopiedApi(false);
|
|
40
44
|
setCopiedEnv(false);
|
|
41
45
|
}, 1500);
|
|
42
46
|
return () => window.clearTimeout(id);
|
|
43
|
-
}, [copiedId, copiedEnv]);
|
|
47
|
+
}, [copiedId, copiedApi, copiedEnv]);
|
|
48
|
+
|
|
49
|
+
const git = useReplicaState().git(workspace.id);
|
|
50
|
+
const docsUrl = publicContentApiUrl({
|
|
51
|
+
apiUrl: publicApiUrl(),
|
|
52
|
+
workspaceId: workspace.id,
|
|
53
|
+
branch: git?.currentBranch,
|
|
54
|
+
defaultBranch: git?.defaultBranch,
|
|
55
|
+
});
|
|
56
|
+
const branchLocked = Boolean(
|
|
57
|
+
git?.currentBranch && git.defaultBranch && git.currentBranch !== git.defaultBranch,
|
|
58
|
+
);
|
|
44
59
|
|
|
45
60
|
const envSnippet = hostEnvSnippet({
|
|
46
61
|
workspaceId: workspace.id,
|
|
@@ -197,6 +212,38 @@ export function SettingsGeneralPage() {
|
|
|
197
212
|
backmatter.config.ts workspaceId · public URLs use this id, not the slug
|
|
198
213
|
</p>
|
|
199
214
|
</Panel>
|
|
215
|
+
<Panel>
|
|
216
|
+
<Text as="h2" size="lg" weight="medium">
|
|
217
|
+
Content API
|
|
218
|
+
</Text>
|
|
219
|
+
<div {...stylex.props(styles.idRow)}>
|
|
220
|
+
<a
|
|
221
|
+
href={docsUrl}
|
|
222
|
+
target="_blank"
|
|
223
|
+
rel="noreferrer"
|
|
224
|
+
{...stylex.props(styles.pre, styles.idPre, styles.apiLink)}
|
|
225
|
+
>
|
|
226
|
+
{docsUrl}
|
|
227
|
+
</a>
|
|
228
|
+
<Button
|
|
229
|
+
variant="secondary"
|
|
230
|
+
aria-label={copiedApi ? "Copied content API URL" : "Copy content API URL"}
|
|
231
|
+
onClick={() => {
|
|
232
|
+
void navigator.clipboard.writeText(docsUrl).then(
|
|
233
|
+
() => setCopiedApi(true),
|
|
234
|
+
() => setCopiedApi(false),
|
|
235
|
+
);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{copiedApi ? "Copied" : "Copy"}
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
<p {...stylex.props(styles.caption)}>
|
|
242
|
+
{branchLocked
|
|
243
|
+
? "This branch needs the content token below."
|
|
244
|
+
: "Default branch is public — no token. URLs use the workspace id."}
|
|
245
|
+
</p>
|
|
246
|
+
</Panel>
|
|
200
247
|
<Panel>
|
|
201
248
|
<Text as="h2" size="lg" weight="medium">
|
|
202
249
|
Host environment
|
|
@@ -358,6 +405,8 @@ export function relativeTime(iso: string, now = Date.now()): string {
|
|
|
358
405
|
return `${Math.round(hours / 24)}d ago`;
|
|
359
406
|
}
|
|
360
407
|
|
|
408
|
+
const hover = "@media (hover: hover)";
|
|
409
|
+
|
|
361
410
|
const styles = stylex.create({
|
|
362
411
|
block: {
|
|
363
412
|
marginTop: space.md,
|
|
@@ -379,6 +428,22 @@ const styles = stylex.create({
|
|
|
379
428
|
flex: 1,
|
|
380
429
|
minWidth: 0,
|
|
381
430
|
},
|
|
431
|
+
apiLink: {
|
|
432
|
+
display: "block",
|
|
433
|
+
color: colors.text,
|
|
434
|
+
textDecoration: "none",
|
|
435
|
+
[hover]: {
|
|
436
|
+
":hover": {
|
|
437
|
+
textDecoration: "underline",
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
":focus-visible": {
|
|
441
|
+
outlineWidth: 2,
|
|
442
|
+
outlineStyle: "solid",
|
|
443
|
+
outlineColor: colors.focus,
|
|
444
|
+
outlineOffset: 2,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
382
447
|
caption: {
|
|
383
448
|
marginTop: space.sm,
|
|
384
449
|
marginBottom: 0,
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// packages/studio/src/pages/WorkspaceHomePage.tsx
|
|
2
|
+
import { Link, useOutletContext } from "react-router";
|
|
3
|
+
import * as stylex from "@stylexjs/stylex";
|
|
4
|
+
import { useReplicaState } from "../replica/useReplica";
|
|
5
|
+
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
6
|
+
import { PageHeader } from "../ui/Page";
|
|
7
|
+
import { Text } from "../ui/Text";
|
|
8
|
+
import { codeSessionEntry } from "../code-session-entry";
|
|
9
|
+
import type { WorkspaceOutletContext } from "../workspace/types";
|
|
10
|
+
import { homeSummary, commitAge, type HomeRow, type HomeSummary } from "./home-rows";
|
|
11
|
+
import type { RecentCommit } from "../api";
|
|
12
|
+
|
|
13
|
+
const hover = "@media (hover: hover)";
|
|
14
|
+
|
|
15
|
+
export function UncommittedHome({
|
|
16
|
+
summary,
|
|
17
|
+
codeHref = null,
|
|
18
|
+
}: {
|
|
19
|
+
summary: HomeSummary;
|
|
20
|
+
codeHref?: string | null;
|
|
21
|
+
}) {
|
|
22
|
+
const branchDetail =
|
|
23
|
+
summary.branch === summary.defaultBranch
|
|
24
|
+
? "Default branch"
|
|
25
|
+
: `Default is ${summary.defaultBranch}`;
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<PageHeader title={summary.name} description={`On ${summary.branch}`} />
|
|
29
|
+
<div {...stylex.props(styles.grid)}>
|
|
30
|
+
<UncommittedCard summary={summary} />
|
|
31
|
+
<CommitsCard summary={summary} />
|
|
32
|
+
<FactCard
|
|
33
|
+
kicker="Models"
|
|
34
|
+
value={summary.modelCount === 0 ? "None yet" : String(summary.modelCount)}
|
|
35
|
+
detail={
|
|
36
|
+
summary.modelCount === 0
|
|
37
|
+
? "Pull from GitHub to load models."
|
|
38
|
+
: "Loaded from the repository."
|
|
39
|
+
}
|
|
40
|
+
href={summary.schemaHref}
|
|
41
|
+
/>
|
|
42
|
+
<FactCard
|
|
43
|
+
kicker="Branch"
|
|
44
|
+
value={summary.branch}
|
|
45
|
+
detail={branchDetail}
|
|
46
|
+
href={summary.githubHref}
|
|
47
|
+
/>
|
|
48
|
+
<FactCard
|
|
49
|
+
kicker="Repository"
|
|
50
|
+
value={summary.repository ?? "Not connected"}
|
|
51
|
+
detail={summary.repository ? "GitHub" : "Connect a repository."}
|
|
52
|
+
href={summary.githubHref}
|
|
53
|
+
/>
|
|
54
|
+
<FactCard
|
|
55
|
+
kicker="Site"
|
|
56
|
+
value={summary.siteUrl ?? "No site URL"}
|
|
57
|
+
detail={summary.siteUrl ? "Public origin" : "Add the site origin."}
|
|
58
|
+
href={summary.generalHref}
|
|
59
|
+
/>
|
|
60
|
+
{codeHref ? (
|
|
61
|
+
<FactCard
|
|
62
|
+
kicker="Code"
|
|
63
|
+
value="Session"
|
|
64
|
+
detail="Host on the left, chat on the right."
|
|
65
|
+
href={codeHref}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
68
|
+
</div>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function UncommittedCard({ summary }: { summary: HomeSummary }) {
|
|
74
|
+
const detail =
|
|
75
|
+
summary.uncommitted === 0
|
|
76
|
+
? "Your working tree is clean."
|
|
77
|
+
: summary.conflicts === 0
|
|
78
|
+
? "No conflicts."
|
|
79
|
+
: summary.conflicts === 1
|
|
80
|
+
? "1 conflict"
|
|
81
|
+
: `${summary.conflicts} conflicts`;
|
|
82
|
+
return (
|
|
83
|
+
<section {...stylex.props(styles.card, styles.span)}>
|
|
84
|
+
<Text size="sm" tone="muted" weight="medium">
|
|
85
|
+
Uncommitted
|
|
86
|
+
</Text>
|
|
87
|
+
<p {...stylex.props(styles.value)}>{summary.uncommitted}</p>
|
|
88
|
+
<Text size="sm" tone={summary.conflicts > 0 ? "danger" : "muted"}>
|
|
89
|
+
{detail}
|
|
90
|
+
</Text>
|
|
91
|
+
{summary.recent.length > 0 ? (
|
|
92
|
+
<>
|
|
93
|
+
<Text size="sm" tone="muted">
|
|
94
|
+
Recently changed
|
|
95
|
+
</Text>
|
|
96
|
+
<ul {...stylex.props(styles.recent)}>
|
|
97
|
+
{summary.recent.map((row) => (
|
|
98
|
+
<li key={row.key}>
|
|
99
|
+
<RecentRow row={row} />
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
</>
|
|
104
|
+
) : null}
|
|
105
|
+
</section>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function CommitsCard({ summary }: { summary: HomeSummary }) {
|
|
110
|
+
if (!summary.commits) return null;
|
|
111
|
+
return (
|
|
112
|
+
<section {...stylex.props(styles.card, styles.span)}>
|
|
113
|
+
<Text size="sm" tone="muted" weight="medium">
|
|
114
|
+
Lately
|
|
115
|
+
</Text>
|
|
116
|
+
{summary.commits.length === 0 ? (
|
|
117
|
+
<Text size="sm" tone="muted">
|
|
118
|
+
No commits on this branch yet.
|
|
119
|
+
</Text>
|
|
120
|
+
) : (
|
|
121
|
+
<ul {...stylex.props(styles.recent)}>
|
|
122
|
+
{summary.commits.map((commit) => (
|
|
123
|
+
<li key={commit.sha}>
|
|
124
|
+
<CommitRow commit={commit} repository={summary.repository} />
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
)}
|
|
129
|
+
</section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function CommitRow({
|
|
134
|
+
commit,
|
|
135
|
+
repository,
|
|
136
|
+
}: {
|
|
137
|
+
commit: RecentCommit;
|
|
138
|
+
repository: string | null;
|
|
139
|
+
}) {
|
|
140
|
+
const when = commitAge(commit.committedAt, Date.now());
|
|
141
|
+
const who = [commit.author, when, commit.cms ? "CMS" : ""].filter(Boolean).join(" · ");
|
|
142
|
+
const body = (
|
|
143
|
+
<>
|
|
144
|
+
<span {...stylex.props(styles.recentName)}>{commit.message}</span>
|
|
145
|
+
<span {...stylex.props(styles.recentStatus)}>{who}</span>
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
if (!repository) return <div {...stylex.props(styles.recentRow)}>{body}</div>;
|
|
149
|
+
return (
|
|
150
|
+
<a
|
|
151
|
+
href={`https://github.com/${repository}/commit/${commit.sha}`}
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noreferrer"
|
|
154
|
+
{...stylex.props(styles.recentRow, styles.recentLink)}
|
|
155
|
+
>
|
|
156
|
+
{body}
|
|
157
|
+
</a>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function RecentRow({ row }: { row: HomeRow }) {
|
|
162
|
+
const body = (
|
|
163
|
+
<>
|
|
164
|
+
<span {...stylex.props(styles.recentName, row.route ? styles.recentRoute : null)}>
|
|
165
|
+
{row.route ?? row.label}
|
|
166
|
+
</span>
|
|
167
|
+
<span {...stylex.props(styles.recentStatus)}>
|
|
168
|
+
{row.route ? row.status : `data · ${row.status}`}
|
|
169
|
+
</span>
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
if (!row.href) return <div {...stylex.props(styles.recentRow)}>{body}</div>;
|
|
173
|
+
return (
|
|
174
|
+
<Link to={row.href} {...stylex.props(styles.recentRow, styles.recentLink)}>
|
|
175
|
+
{body}
|
|
176
|
+
</Link>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function FactCard({
|
|
181
|
+
kicker,
|
|
182
|
+
value,
|
|
183
|
+
detail,
|
|
184
|
+
href,
|
|
185
|
+
}: {
|
|
186
|
+
kicker: string;
|
|
187
|
+
value: string;
|
|
188
|
+
detail: string;
|
|
189
|
+
href: string;
|
|
190
|
+
}) {
|
|
191
|
+
return (
|
|
192
|
+
<Link to={href} {...stylex.props(styles.card, styles.cardLink)}>
|
|
193
|
+
<Text size="sm" tone="muted" weight="medium">
|
|
194
|
+
{kicker}
|
|
195
|
+
</Text>
|
|
196
|
+
<p {...stylex.props(styles.factValue)}>{value}</p>
|
|
197
|
+
<Text size="sm" tone="muted">
|
|
198
|
+
{detail}
|
|
199
|
+
</Text>
|
|
200
|
+
</Link>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function WorkspaceHomePage() {
|
|
205
|
+
const { workspace, models } = useOutletContext<WorkspaceOutletContext>();
|
|
206
|
+
const git = useReplicaState().git(workspace.id);
|
|
207
|
+
const summary = homeSummary({
|
|
208
|
+
name: workspace.name,
|
|
209
|
+
slug: workspace.slug,
|
|
210
|
+
branch: git?.currentBranch ?? "main",
|
|
211
|
+
defaultBranch: git?.defaultBranch ?? git?.currentBranch ?? "main",
|
|
212
|
+
repository: workspace.githubRepoFullName,
|
|
213
|
+
siteUrl: workspace.siteUrls[0] ?? null,
|
|
214
|
+
modelCount: models.length,
|
|
215
|
+
changes: git?.fetchedChanges ?? git?.changes ?? [],
|
|
216
|
+
models,
|
|
217
|
+
commits: git?.recentCommits ?? null,
|
|
218
|
+
});
|
|
219
|
+
return (
|
|
220
|
+
<UncommittedHome
|
|
221
|
+
summary={summary}
|
|
222
|
+
codeHref={codeSessionEntry(workspace.role, workspace.slug)}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const styles = stylex.create({
|
|
228
|
+
grid: {
|
|
229
|
+
display: "grid",
|
|
230
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
231
|
+
gap: space.lg,
|
|
232
|
+
},
|
|
233
|
+
span: {
|
|
234
|
+
gridColumn: "1 / -1",
|
|
235
|
+
},
|
|
236
|
+
card: {
|
|
237
|
+
display: "flex",
|
|
238
|
+
flexDirection: "column",
|
|
239
|
+
gap: space.sm,
|
|
240
|
+
minWidth: 0,
|
|
241
|
+
borderWidth: 1,
|
|
242
|
+
borderStyle: "solid",
|
|
243
|
+
borderColor: colors.border,
|
|
244
|
+
borderRadius: radius.md,
|
|
245
|
+
backgroundColor: colors.bgSurface,
|
|
246
|
+
padding: space.xl,
|
|
247
|
+
textDecoration: "none",
|
|
248
|
+
color: colors.text,
|
|
249
|
+
},
|
|
250
|
+
cardLink: {
|
|
251
|
+
[hover]: {
|
|
252
|
+
":hover": {
|
|
253
|
+
backgroundColor: colors.bgHover,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
value: {
|
|
258
|
+
margin: 0,
|
|
259
|
+
fontSize: "32px",
|
|
260
|
+
fontWeight: 600,
|
|
261
|
+
letterSpacing: "-0.03em",
|
|
262
|
+
lineHeight: 1,
|
|
263
|
+
},
|
|
264
|
+
factValue: {
|
|
265
|
+
margin: 0,
|
|
266
|
+
overflow: "hidden",
|
|
267
|
+
fontSize: type.lg,
|
|
268
|
+
fontWeight: 600,
|
|
269
|
+
letterSpacing: "-0.02em",
|
|
270
|
+
textOverflow: "ellipsis",
|
|
271
|
+
whiteSpace: "nowrap",
|
|
272
|
+
},
|
|
273
|
+
recent: {
|
|
274
|
+
display: "flex",
|
|
275
|
+
flexDirection: "column",
|
|
276
|
+
gap: space.xs,
|
|
277
|
+
margin: 0,
|
|
278
|
+
marginTop: space.md,
|
|
279
|
+
padding: 0,
|
|
280
|
+
listStyle: "none",
|
|
281
|
+
},
|
|
282
|
+
recentRow: {
|
|
283
|
+
display: "flex",
|
|
284
|
+
alignItems: "center",
|
|
285
|
+
justifyContent: "space-between",
|
|
286
|
+
gap: space.lg,
|
|
287
|
+
minHeight: "28px",
|
|
288
|
+
color: colors.text,
|
|
289
|
+
textDecoration: "none",
|
|
290
|
+
},
|
|
291
|
+
recentLink: {
|
|
292
|
+
[hover]: {
|
|
293
|
+
":hover": {
|
|
294
|
+
color: colors.textMuted,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
recentName: {
|
|
299
|
+
overflow: "hidden",
|
|
300
|
+
textOverflow: "ellipsis",
|
|
301
|
+
whiteSpace: "nowrap",
|
|
302
|
+
fontSize: type.md,
|
|
303
|
+
},
|
|
304
|
+
recentRoute: {
|
|
305
|
+
fontFamily: type.mono,
|
|
306
|
+
fontSize: type.sm,
|
|
307
|
+
},
|
|
308
|
+
recentStatus: {
|
|
309
|
+
flexShrink: 0,
|
|
310
|
+
fontSize: type.sm,
|
|
311
|
+
color: colors.textMuted,
|
|
312
|
+
},
|
|
313
|
+
});
|