@backmatter/studio 0.0.15 → 0.0.17

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 (80) hide show
  1. package/dist/chunks/{UiKitApp-BDsrXN12.js → UiKitApp-D7vllRQE.js} +2 -2
  2. package/dist/chunks/{WorkspaceRoutes-BTZ1Ah4F.js → WorkspaceRoutes-DH1LD-2Q.js} +30013 -27965
  3. package/dist/chunks/{dashboard-FT7dm5wi.js → dashboard-Dsaeud2x.js} +55 -54
  4. package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
  5. package/dist/chunks/{index-DHJvh5Uy.js → index-CGMqw2NH.js} +1 -1
  6. package/dist/dashboard.js +2 -2
  7. package/dist/define.js +1 -1
  8. package/dist/index.js +4 -4
  9. package/dist/site-widget.css +49 -1
  10. package/dist/site-widget.js +8341 -8058
  11. package/dist/studio.css +279 -13
  12. package/package.json +2 -2
  13. package/src/WorkspaceRoutes.tsx +5 -2
  14. package/src/api.ts +82 -7
  15. package/src/chat/ChangeCard.tsx +8 -2
  16. package/src/chat/ChatMessage.tsx +171 -102
  17. package/src/chat/ChatPanel.tsx +54 -34
  18. package/src/chat/ChatProvider.tsx +122 -20
  19. package/src/chat/Composer.tsx +51 -8
  20. package/src/chat/PointerChip.tsx +9 -5
  21. package/src/chat/ThreadHistory.tsx +3 -3
  22. package/src/chat/chat-context.ts +67 -3
  23. package/src/chat/chat-helpers.ts +12 -0
  24. package/src/chat/chat-state.ts +11 -0
  25. package/src/chrome/AppShell.tsx +16 -5
  26. package/src/chrome/CommandPalette.tsx +12 -0
  27. package/src/chrome/ContentExplorerSidebar.tsx +23 -3
  28. package/src/chrome/Toast.tsx +115 -0
  29. package/src/chrome/UserMenu.tsx +34 -3
  30. package/src/chrome/WorkspaceStatusBar.tsx +221 -30
  31. package/src/chrome/commit-error.ts +13 -0
  32. package/src/chrome/content-api-url.ts +0 -78
  33. package/src/chrome/content-tree-visibility.ts +13 -3
  34. package/src/chrome/explorer-icons.tsx +5 -0
  35. package/src/chrome/pull-failure.ts +61 -0
  36. package/src/code-session-entry.ts +9 -0
  37. package/src/components/WorkspaceSwitcher.tsx +2 -2
  38. package/src/element-origin.ts +149 -0
  39. package/src/fields/FieldControl.tsx +748 -0
  40. package/src/fields/choose-field-input.ts +64 -0
  41. package/src/fields/load-relation-documents.ts +35 -0
  42. package/src/fields/omit-field.ts +12 -0
  43. package/src/fields/relation-targets.ts +44 -0
  44. package/src/fields/resolve-object-fields.ts +21 -0
  45. package/src/host-pointer-bootstrap.ts +11 -0
  46. package/src/host-pointer.ts +147 -0
  47. package/src/install-preview-pointer.ts +19 -3
  48. package/src/pages/BlockSettingsDialog.tsx +19 -115
  49. package/src/pages/BlocksList.tsx +43 -35
  50. package/src/pages/BodyProse.tsx +16 -1
  51. package/src/pages/CodeSessionFiles.tsx +221 -0
  52. package/src/pages/CodeSessionPage.tsx +1361 -0
  53. package/src/pages/DocEditorPage.tsx +120 -107
  54. package/src/pages/PreviewSize.tsx +181 -0
  55. package/src/pages/RenderedBody.tsx +17 -2
  56. package/src/pages/SettingsGeneralPage.tsx +67 -2
  57. package/src/pages/WorkspaceHomePage.tsx +313 -0
  58. package/src/pages/WorkspaceLayout.tsx +10 -1
  59. package/src/pages/branch-notice.ts +16 -0
  60. package/src/pages/code-session-boot.ts +43 -0
  61. package/src/pages/code-session-context.tsx +53 -0
  62. package/src/pages/code-session-files.ts +42 -0
  63. package/src/pages/code-session-frame.ts +52 -0
  64. package/src/pages/code-session-routes.ts +42 -0
  65. package/src/pages/home-rows.ts +140 -0
  66. package/src/pages/preview-size.ts +33 -0
  67. package/src/pages/session-preview.ts +109 -0
  68. package/src/pages/use-session-preview.ts +70 -0
  69. package/src/replica/store.ts +11 -0
  70. package/src/replica/types.ts +6 -0
  71. package/src/shell-resets.css +15 -0
  72. package/src/site-pointer.ts +24 -5
  73. package/src/studio-config.ts +1 -1
  74. package/src/ui/Input.tsx +178 -20
  75. package/src/ui/ListRow.tsx +12 -3
  76. package/src/ui/Page.tsx +12 -1
  77. package/src/ui/popup.ts +11 -2
  78. package/src/workspace-path.ts +35 -3
  79. package/src/chrome/ContentApiPopover.tsx +0 -215
  80. package/src/pages/WorkspaceHomeRedirect.tsx +0 -17
package/src/ui/Input.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  // packages/studio/src/ui/Input.tsx
2
- import type { ComponentPropsWithoutRef } from "react";
2
+ import { useState, type ComponentPropsWithoutRef } from "react";
3
3
  import { Input as BaseInput } from "@base-ui/react/input";
4
4
  import { Select as BaseSelect } from "@base-ui/react/select";
5
5
  import * as stylex from "@stylexjs/stylex";
@@ -9,7 +9,7 @@ import { cx, popup, popupClassName } from "./popup";
9
9
  type Props = Omit<ComponentPropsWithoutRef<"input">, "className" | "style">;
10
10
 
11
11
  export function Input(props: Props) {
12
- return <BaseInput {...props} {...stylex.props(styles.input)} />;
12
+ return <BaseInput {...stylex.props(styles.input, styles.inputFocus)} {...props} />;
13
13
  }
14
14
 
15
15
  export type SelectOption = { value: string; label: string; disabled?: boolean };
@@ -22,6 +22,9 @@ export function Select({
22
22
  placeholder = "Select…",
23
23
  name,
24
24
  disabled,
25
+ chip = false,
26
+ clearable = false,
27
+ searchable = false,
25
28
  "aria-label": ariaLabel,
26
29
  id,
27
30
  }: {
@@ -31,34 +34,100 @@ export function Select({
31
34
  placeholder?: string;
32
35
  name?: string;
33
36
  disabled?: boolean;
37
+ /** Selected value sits in the field as a removable chip. */
38
+ chip?: boolean;
39
+ /** Chip × and the field clear control clear the value. Ignored when `chip` is false. */
40
+ clearable?: boolean;
41
+ /** Filter the menu by label or value. The closed field stays a select. */
42
+ searchable?: boolean;
34
43
  "aria-label"?: string;
35
44
  id?: string;
36
45
  }) {
46
+ const selected = options.find((opt) => opt.value === value) ?? null;
47
+ const [search, setSearch] = useState("");
48
+ const query = search.trim().toLowerCase();
49
+ const visible = query
50
+ ? options.filter(
51
+ (opt) =>
52
+ opt.label.toLowerCase().includes(query) || opt.value.toLowerCase().includes(query),
53
+ )
54
+ : options;
55
+ const clear = (event: { preventDefault: () => void; stopPropagation: () => void }) => {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ onValueChange(null);
59
+ };
37
60
  return (
38
61
  <BaseSelect.Root
39
62
  items={options}
40
63
  value={value}
41
64
  onValueChange={(next) => onValueChange(typeof next === "string" ? next : null)}
65
+ onOpenChange={(open) => {
66
+ if (!open) setSearch("");
67
+ }}
42
68
  name={name}
43
69
  disabled={disabled}
44
70
  >
45
71
  <BaseSelect.Trigger
46
72
  id={id}
47
73
  aria-label={ariaLabel}
74
+ data-no-ring=""
48
75
  className={cx(styles.input, styles.selectTrigger)}
49
76
  >
50
- <BaseSelect.Value placeholder={placeholder} className={cx(styles.selectValue)} />
51
- <BaseSelect.Icon className={cx(styles.selectIcon)}>
52
- <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
53
- <path
54
- d="M4.5 6.25 8 9.75l3.5-3.5"
55
- stroke="currentColor"
56
- strokeWidth="1.5"
57
- strokeLinecap="round"
58
- strokeLinejoin="round"
59
- />
60
- </svg>
61
- </BaseSelect.Icon>
77
+ {chip && selected ? (
78
+ <span {...stylex.props(styles.chip)}>
79
+ <span {...stylex.props(styles.chipLabel)}>{selected.label}</span>
80
+ {clearable && !disabled ? (
81
+ <span
82
+ data-select-clear
83
+ aria-hidden
84
+ {...stylex.props(styles.chipRemove)}
85
+ onPointerDown={(event) => event.stopPropagation()}
86
+ onClick={clear}
87
+ >
88
+ ×
89
+ </span>
90
+ ) : null}
91
+ </span>
92
+ ) : (
93
+ <BaseSelect.Value
94
+ placeholder={placeholder}
95
+ className={cx(styles.selectValue, chip && styles.placeholder)}
96
+ />
97
+ )}
98
+ <span {...stylex.props(styles.selectActions)}>
99
+ {chip && clearable && selected && !disabled ? (
100
+ <span
101
+ data-select-clear
102
+ role="button"
103
+ aria-label="Clear"
104
+ {...stylex.props(styles.fieldClear)}
105
+ onPointerDown={(event) => event.stopPropagation()}
106
+ onClick={clear}
107
+ >
108
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
109
+ <circle cx="8" cy="8" r="6.25" stroke="currentColor" strokeWidth="1.25" />
110
+ <path
111
+ d="M6 6l4 4M10 6l-4 4"
112
+ stroke="currentColor"
113
+ strokeWidth="1.25"
114
+ strokeLinecap="round"
115
+ />
116
+ </svg>
117
+ </span>
118
+ ) : null}
119
+ <BaseSelect.Icon className={cx(styles.selectIcon)}>
120
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
121
+ <path
122
+ d="M4.5 6.25 8 9.75l3.5-3.5"
123
+ stroke="currentColor"
124
+ strokeWidth="1.5"
125
+ strokeLinecap="round"
126
+ strokeLinejoin="round"
127
+ />
128
+ </svg>
129
+ </BaseSelect.Icon>
130
+ </span>
62
131
  </BaseSelect.Trigger>
63
132
  <BaseSelect.Portal>
64
133
  <BaseSelect.Positioner
@@ -69,8 +138,27 @@ export function Select({
69
138
  className={cx(popup.positioner)}
70
139
  >
71
140
  <BaseSelect.Popup className={(state) => popupClassName(state, styles.selectPopup)}>
141
+ {searchable ? (
142
+ <input
143
+ data-select-search
144
+ data-no-ring=""
145
+ value={search}
146
+ placeholder="Search"
147
+ spellCheck={false}
148
+ autoComplete="off"
149
+ className={cx(popup.searchInput)}
150
+ onChange={(event) => setSearch(event.target.value)}
151
+ onMouseDown={(event) => {
152
+ event.preventDefault();
153
+ event.stopPropagation();
154
+ event.currentTarget.focus();
155
+ }}
156
+ onPointerDown={(event) => event.stopPropagation()}
157
+ onKeyDown={(event) => event.stopPropagation()}
158
+ />
159
+ ) : null}
72
160
  <BaseSelect.List>
73
- {options.map((opt) => (
161
+ {visible.map((opt) => (
74
162
  <BaseSelect.Item
75
163
  key={opt.value}
76
164
  value={opt.value}
@@ -115,10 +203,10 @@ export function Textarea({
115
203
  }: Omit<ComponentPropsWithoutRef<"textarea">, "className" | "style"> & {
116
204
  mono?: boolean;
117
205
  }) {
118
- return (
206
+ return (
119
207
  <textarea
120
208
  {...props}
121
- {...stylex.props(styles.input, styles.textarea, mono && styles.mono)}
209
+ {...stylex.props(styles.input, styles.inputFocus, styles.textarea, mono && styles.mono)}
122
210
  />
123
211
  );
124
212
  }
@@ -137,6 +225,11 @@ const styles = stylex.create({
137
225
  color: colors.text,
138
226
  fontSize: type.md,
139
227
  outline: "none",
228
+ ":disabled": {
229
+ opacity: 0.55,
230
+ },
231
+ },
232
+ inputFocus: {
140
233
  ":focus": {
141
234
  borderColor: colors.focus,
142
235
  outlineWidth: 2,
@@ -144,9 +237,6 @@ const styles = stylex.create({
144
237
  outlineColor: colors.focus,
145
238
  outlineOffset: -1,
146
239
  },
147
- ":disabled": {
148
- opacity: 0.55,
149
- },
150
240
  },
151
241
  selectTrigger: {
152
242
  display: "flex",
@@ -156,6 +246,22 @@ const styles = stylex.create({
156
246
  textAlign: "left",
157
247
  fontFamily: type.sans,
158
248
  cursor: "pointer",
249
+ outline: "none",
250
+ boxShadow: "none",
251
+ ":focus": {
252
+ borderColor: colors.border,
253
+ outlineStyle: "none",
254
+ outlineWidth: 0,
255
+ outlineOffset: 0,
256
+ boxShadow: "none",
257
+ },
258
+ ":focus-visible": {
259
+ borderColor: colors.border,
260
+ outlineStyle: "none",
261
+ outlineWidth: 0,
262
+ outlineOffset: 0,
263
+ boxShadow: "none",
264
+ },
159
265
  },
160
266
  selectValue: {
161
267
  minWidth: 0,
@@ -163,6 +269,58 @@ const styles = stylex.create({
163
269
  textOverflow: "ellipsis",
164
270
  whiteSpace: "nowrap",
165
271
  },
272
+ placeholder: {
273
+ color: colors.textMuted,
274
+ },
275
+ chip: {
276
+ display: "inline-flex",
277
+ alignItems: "center",
278
+ gap: space.xs,
279
+ maxWidth: "100%",
280
+ minHeight: "22px",
281
+ paddingInline: space.sm,
282
+ paddingBlock: "1px",
283
+ borderWidth: 1,
284
+ borderStyle: "solid",
285
+ borderColor: colors.border,
286
+ borderRadius: radius.sm,
287
+ backgroundColor: colors.bgHover,
288
+ color: colors.text,
289
+ fontSize: type.sm,
290
+ },
291
+ chipLabel: {
292
+ minWidth: 0,
293
+ overflow: "hidden",
294
+ textOverflow: "ellipsis",
295
+ whiteSpace: "nowrap",
296
+ },
297
+ chipRemove: {
298
+ display: "inline-flex",
299
+ alignItems: "center",
300
+ justifyContent: "center",
301
+ margin: 0,
302
+ padding: 0,
303
+ borderWidth: 0,
304
+ backgroundColor: "transparent",
305
+ color: colors.textMuted,
306
+ fontSize: type.sm,
307
+ lineHeight: 1,
308
+ cursor: "pointer",
309
+ },
310
+ selectActions: {
311
+ display: "inline-flex",
312
+ alignItems: "center",
313
+ gap: space.xs,
314
+ flexShrink: 0,
315
+ marginLeft: "auto",
316
+ },
317
+ fieldClear: {
318
+ display: "inline-flex",
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ color: colors.textMuted,
322
+ cursor: "pointer",
323
+ },
166
324
  selectIcon: {
167
325
  display: "inline-flex",
168
326
  flexShrink: 0,
@@ -11,13 +11,13 @@ export function ListRow({
11
11
  meta,
12
12
  trailing,
13
13
  }: {
14
- to: string;
14
+ to?: string;
15
15
  title: string;
16
16
  meta?: string;
17
17
  trailing?: string;
18
18
  }) {
19
- return (
20
- <Link to={to} {...stylex.props(styles.row)}>
19
+ const body = (
20
+ <>
21
21
  <span {...stylex.props(styles.body)}>
22
22
  <span {...stylex.props(styles.title)}>{title}</span>
23
23
  {meta ? <span {...stylex.props(styles.meta)}>{meta}</span> : null}
@@ -25,6 +25,12 @@ export function ListRow({
25
25
  {trailing ? (
26
26
  <span {...stylex.props(styles.trailing)}>{trailing}</span>
27
27
  ) : null}
28
+ </>
29
+ );
30
+ if (!to) return <div {...stylex.props(styles.row, styles.static)}>{body}</div>;
31
+ return (
32
+ <Link to={to} {...stylex.props(styles.row)}>
33
+ {body}
28
34
  </Link>
29
35
  );
30
36
  }
@@ -66,6 +72,9 @@ const styles = stylex.create({
66
72
  },
67
73
  },
68
74
  },
75
+ static: {
76
+ cursor: "default",
77
+ },
69
78
  body: {
70
79
  display: "flex",
71
80
  minWidth: 0,
package/src/ui/Page.tsx CHANGED
@@ -6,10 +6,12 @@ export function PageFrame({
6
6
  children,
7
7
  flush,
8
8
  centered,
9
+ wide,
9
10
  }: {
10
11
  children: React.ReactNode;
11
12
  flush?: boolean;
12
13
  centered?: boolean;
14
+ wide?: boolean;
13
15
  }) {
14
16
  return (
15
17
  <div
@@ -22,7 +24,13 @@ export function PageFrame({
22
24
  {flush ? (
23
25
  children
24
26
  ) : (
25
- <div {...stylex.props(styles.inner, centered && styles.innerCentered)}>
27
+ <div
28
+ {...stylex.props(
29
+ styles.inner,
30
+ centered && styles.innerCentered,
31
+ wide && styles.innerWide,
32
+ )}
33
+ >
26
34
  {children}
27
35
  </div>
28
36
  )}
@@ -108,6 +116,9 @@ const styles = stylex.create({
108
116
  innerCentered: {
109
117
  maxWidth: "440px",
110
118
  },
119
+ innerWide: {
120
+ maxWidth: "960px",
121
+ },
111
122
  header: {
112
123
  display: "flex",
113
124
  alignItems: "flex-start",
package/src/ui/popup.ts CHANGED
@@ -183,11 +183,20 @@ export const popup = stylex.create({
183
183
  fontFamily: type.sans,
184
184
  fontSize: type.sm,
185
185
  outline: "none",
186
+ boxShadow: "none",
186
187
  ":focus": {
187
- outline: "none",
188
+ outlineStyle: "none",
189
+ outlineWidth: 0,
190
+ outlineOffset: 0,
191
+ boxShadow: "none",
192
+ borderBottomColor: colors.border,
188
193
  },
189
194
  ":focus-visible": {
190
- outline: "none",
195
+ outlineStyle: "none",
196
+ outlineWidth: 0,
197
+ outlineOffset: 0,
198
+ boxShadow: "none",
199
+ borderBottomColor: colors.border,
191
200
  },
192
201
  "::placeholder": {
193
202
  color: colors.textMuted,
@@ -3,6 +3,14 @@ export function workspaceHomePath(slug: string) {
3
3
  return `/${slug}`;
4
4
  }
5
5
 
6
+ /** The workspace index. Content, media, and settings are deeper routes. */
7
+ export function isWorkspaceHomePath(pathname: string) {
8
+ const slug = cmsWorkspaceSlug(pathname);
9
+ if (!slug) return false;
10
+ const path = pathname.split("?")[0]?.replace(/\/$/, "") || "/";
11
+ return path === `/${slug}`;
12
+ }
13
+
6
14
  export function contentTypePath(slug: string, type: string) {
7
15
  return `/${slug}/content/${type}`;
8
16
  }
@@ -15,6 +23,10 @@ export function mediaPath(slug: string) {
15
23
  return `/${slug}/media`;
16
24
  }
17
25
 
26
+ export function codeSessionPath(slug: string) {
27
+ return `/${slug}/code`;
28
+ }
29
+
18
30
  export function settingsPath(slug: string, page: string) {
19
31
  return `/${slug}/settings/${page}`;
20
32
  }
@@ -71,19 +83,33 @@ export function isWorkspaceMediaPath(pathname: string) {
71
83
  return /\/media\/?$/.test(pathname);
72
84
  }
73
85
 
86
+ export function isWorkspaceCodePath(pathname: string) {
87
+ return /\/code\/?$/.test(pathname);
88
+ }
89
+
74
90
  /** The content file tree belongs on content routes only. */
75
91
  export function isWorkspaceContentPath(pathname: string) {
76
92
  return /\/content(?:\/|$)/.test(pathname);
77
93
  }
78
94
 
95
+ /** The content index. The file tree lives here; the workspace home does not. */
96
+ export function contentIndexPath(slug: string) {
97
+ return `/${slug}/content`;
98
+ }
99
+
100
+ export function isWorkspaceContentIndexPath(pathname: string) {
101
+ const path = pathname.split("?")[0]?.replace(/\/$/, "") || "/";
102
+ return /\/content$/.test(path);
103
+ }
104
+
79
105
  /**
80
106
  * Where the content rail icon navigates.
81
107
  * On a content route it stays put: a document toggles the file tree, and the
82
- * type index is already the file tree.
83
- * Anywhere else (settings, media, connect): the workspace home, which opens the docs.
108
+ * content index is already the file tree.
109
+ * Anywhere else (home, settings, media, connect): `/content`.
84
110
  */
85
111
  export function contentRailHref(pathname: string, slug: string): string | null {
86
- if (!isWorkspaceContentPath(pathname)) return workspaceHomePath(slug);
112
+ if (!isWorkspaceContentPath(pathname)) return contentIndexPath(slug);
87
113
  return null;
88
114
  }
89
115
 
@@ -101,6 +127,12 @@ export function isOrgPickerPath(pathname: string) {
101
127
  return path === "/invite";
102
128
  }
103
129
 
130
+ /** The workspace list and create form. The app sidebar repeats this list. */
131
+ export function isWorkspaceListPath(pathname: string) {
132
+ const path = pathname.split("?")[0]?.replace(/\/$/, "") || "/";
133
+ return path === "/workspaces" || path === "/workspaces/new";
134
+ }
135
+
104
136
  /** CLI approval is a full-screen form, not a workspace. */
105
137
  export function isCliAuthorizePath(pathname: string) {
106
138
  const path = pathname.replace(/\/$/, "") || "/";
@@ -1,215 +0,0 @@
1
- // packages/studio/src/chrome/ContentApiPopover.tsx
2
- import { useEffect, useState } from "react";
3
- import { Popover } from "@base-ui/react/popover";
4
- import * as stylex from "@stylexjs/stylex";
5
- import { publicApiUrl } from "../studio-config";
6
- import { colors, radius, space, type } from "../styles/tokens.stylex";
7
- import { cx, popup, popupClassName } from "../ui/popup";
8
- import { contentApiLinks } from "./content-api-url";
9
- import { ApiIcon } from "./icons";
10
-
11
- const hover = "@media (hover: hover)";
12
-
13
- function CopyUrl({ label, href }: { label: string; href: string }) {
14
- const [copied, setCopied] = useState(false);
15
-
16
- useEffect(() => {
17
- if (!copied) return;
18
- const id = window.setTimeout(() => setCopied(false), 1500);
19
- return () => window.clearTimeout(id);
20
- }, [copied]);
21
-
22
- async function copy() {
23
- try {
24
- await navigator.clipboard.writeText(href);
25
- setCopied(true);
26
- } catch {
27
- setCopied(false);
28
- }
29
- }
30
-
31
- return (
32
- <div {...stylex.props(styles.row)}>
33
- <p {...stylex.props(popup.groupLabel)}>{label}</p>
34
- <div {...stylex.props(styles.urlRow)}>
35
- <a
36
- href={href}
37
- target="_blank"
38
- rel="noreferrer"
39
- {...stylex.props(styles.href)}
40
- >
41
- {href}
42
- </a>
43
- <button
44
- type="button"
45
- onClick={() => void copy()}
46
- aria-label={copied ? "Copied" : `Copy ${label} URL`}
47
- {...stylex.props(styles.copy)}
48
- >
49
- {copied ? "Copied" : "Copy"}
50
- </button>
51
- </div>
52
- </div>
53
- );
54
- }
55
-
56
- export function ContentApiPopover({
57
- workspaceId,
58
- branch,
59
- defaultBranch,
60
- pathname,
61
- search,
62
- }: {
63
- workspaceId: string;
64
- branch?: string | null;
65
- defaultBranch?: string | null;
66
- pathname: string;
67
- search?: string;
68
- }) {
69
- const links = contentApiLinks({
70
- apiUrl: publicApiUrl(),
71
- workspaceId,
72
- branch,
73
- defaultBranch,
74
- pathname,
75
- search,
76
- });
77
- const locked = Boolean(branch && defaultBranch && branch !== defaultBranch);
78
-
79
- return (
80
- <Popover.Root>
81
- <Popover.Trigger aria-label="Content API" className={cx(styles.trigger)}>
82
- <ApiIcon />
83
- <span>API</span>
84
- </Popover.Trigger>
85
- <Popover.Portal>
86
- <Popover.Positioner
87
- side="bottom"
88
- align="start"
89
- sideOffset={6}
90
- className={cx(popup.positioner)}
91
- >
92
- <Popover.Popup
93
- aria-label="Content API"
94
- className={(state) => popupClassName(state, styles.popup)}
95
- >
96
- <p {...stylex.props(popup.groupLabel)}>Content API</p>
97
- <p {...stylex.props(styles.hint)}>
98
- {locked
99
- ? "This branch needs the content token from Settings → General."
100
- : "Default branch is public — no token. URLs use the workspace id."}
101
- </p>
102
- {links.map((link) => (
103
- <CopyUrl key={link.href} label={link.label} href={link.href} />
104
- ))}
105
- </Popover.Popup>
106
- </Popover.Positioner>
107
- </Popover.Portal>
108
- </Popover.Root>
109
- );
110
- }
111
-
112
- const styles = stylex.create({
113
- trigger: {
114
- display: "inline-flex",
115
- alignItems: "center",
116
- gap: space.xs,
117
- minHeight: "24px",
118
- paddingInline: space.xs,
119
- borderWidth: 0,
120
- backgroundColor: "transparent",
121
- color: colors.textMuted,
122
- fontSize: type.xs,
123
- fontFamily: type.mono,
124
- cursor: "pointer",
125
- touchAction: "manipulation",
126
- ":focus-visible": {
127
- outlineWidth: 2,
128
- outlineStyle: "solid",
129
- outlineColor: colors.focus,
130
- outlineOffset: "-2px",
131
- },
132
- [hover]: {
133
- ":hover": {
134
- color: colors.text,
135
- },
136
- },
137
- },
138
- popup: {
139
- width: "380px",
140
- paddingBottom: space.md,
141
- overflow: "hidden",
142
- },
143
- hint: {
144
- margin: 0,
145
- paddingInline: space.lg,
146
- paddingBottom: space.sm,
147
- fontFamily: type.mono,
148
- fontSize: type.xs,
149
- lineHeight: 1.45,
150
- color: colors.textMuted,
151
- },
152
- row: {
153
- display: "flex",
154
- flexDirection: "column",
155
- gap: space.xs,
156
- paddingBottom: space.sm,
157
- },
158
- urlRow: {
159
- display: "flex",
160
- alignItems: "center",
161
- gap: space.sm,
162
- minWidth: 0,
163
- marginInline: space.lg,
164
- },
165
- href: {
166
- minWidth: 0,
167
- flex: 1,
168
- overflow: "hidden",
169
- textOverflow: "ellipsis",
170
- whiteSpace: "nowrap",
171
- color: colors.text,
172
- fontFamily: type.mono,
173
- fontSize: type.xs,
174
- textDecoration: "none",
175
- [hover]: {
176
- ":hover": {
177
- textDecoration: "underline",
178
- },
179
- },
180
- ":focus-visible": {
181
- outlineWidth: 2,
182
- outlineStyle: "solid",
183
- outlineColor: colors.focus,
184
- outlineOffset: 2,
185
- },
186
- },
187
- copy: {
188
- flexShrink: 0,
189
- minWidth: "52px",
190
- minHeight: "24px",
191
- paddingInline: space.sm,
192
- borderWidth: 1,
193
- borderStyle: "solid",
194
- borderColor: colors.border,
195
- borderRadius: radius.md,
196
- backgroundColor: colors.bgSurface,
197
- color: colors.textMuted,
198
- fontSize: type.xs,
199
- fontFamily: type.sans,
200
- cursor: "pointer",
201
- touchAction: "manipulation",
202
- [hover]: {
203
- ":hover": {
204
- color: colors.text,
205
- backgroundColor: colors.bgHover,
206
- },
207
- },
208
- ":focus-visible": {
209
- outlineWidth: 2,
210
- outlineStyle: "solid",
211
- outlineColor: colors.focus,
212
- outlineOffset: 1,
213
- },
214
- },
215
- });
@@ -1,17 +0,0 @@
1
- // packages/studio/src/pages/WorkspaceHomeRedirect.tsx
2
- import { Navigate, useOutletContext } from "react-router";
3
- import type { WorkspaceOutletContext } from "../workspace/types";
4
- import { contentTypePath, settingsPath } from "../workspace-path";
5
-
6
- export function WorkspaceHomeRedirect() {
7
- const { workspace, models } = useOutletContext<WorkspaceOutletContext>();
8
- const first = models[0]?.type;
9
- if (first) {
10
- return (
11
- <Navigate to={contentTypePath(workspace.slug, first)} replace />
12
- );
13
- }
14
- return (
15
- <Navigate to={settingsPath(workspace.slug, "schema")} replace />
16
- );
17
- }