@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
@@ -5,11 +5,12 @@ import { DragDropProvider } from "@dnd-kit/react";
5
5
  import { isSortable, useSortable } from "@dnd-kit/react/sortable";
6
6
  import { Menu } from "@base-ui/react/menu";
7
7
  import * as stylex from "@stylexjs/stylex";
8
- import type { BlockSnapshot } from "../api";
9
- import { resolveFieldInput, useBlockConfig, useStudioDefinition } from "../define-studio";
8
+ import type { BlockSnapshot, SchemaField } from "../api";
9
+ import { useBlockConfig } from "../define-studio";
10
+ import { FieldControl } from "../fields/FieldControl";
11
+ import { omitField } from "../fields/omit-field";
10
12
  import { Button } from "../ui/Button";
11
13
  import { Field } from "../ui/Field";
12
- import { Input } from "../ui/Input";
13
14
  import { Text } from "../ui/Text";
14
15
  import { cx, popup, popupClassName } from "../ui/popup";
15
16
  import { colors, radius, space } from "../styles/tokens.stylex";
@@ -37,10 +38,17 @@ function sortTransition() {
37
38
  : SORT_TRANSITION;
38
39
  }
39
40
 
40
- function fieldLabel(name: string) {
41
+ function fieldLabel(name: string, field: SchemaField) {
42
+ if ("label" in field && typeof field.label === "string" && field.label) return field.label;
41
43
  return name.charAt(0).toUpperCase() + name.slice(1);
42
44
  }
43
45
 
46
+ type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
47
+ type RelationDocumentLists = Record<
48
+ string,
49
+ Array<{ slug: string; frontmatter: Record<string, unknown> }>
50
+ >;
51
+
44
52
  function GripIcon() {
45
53
  return (
46
54
  <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden>
@@ -58,6 +66,9 @@ function BlockItem({
58
66
  block,
59
67
  index,
60
68
  catalog,
69
+ models,
70
+ objects,
71
+ documentLists,
61
72
  open,
62
73
  readOnly,
63
74
  onToggle,
@@ -67,6 +78,9 @@ function BlockItem({
67
78
  block: BlockInstance;
68
79
  index: number;
69
80
  catalog: BlockSnapshot[];
81
+ models: ModelRef[];
82
+ objects: Array<{ name: string; fields: Record<string, SchemaField> }>;
83
+ documentLists?: RelationDocumentLists;
70
84
  open: boolean;
71
85
  readOnly?: boolean;
72
86
  onToggle: () => void;
@@ -83,8 +97,7 @@ function BlockItem({
83
97
  transition: sortTransition(),
84
98
  });
85
99
  const def = catalog.find((b) => b.name === block.type);
86
- const fields = Object.keys(def?.fields ?? {});
87
- const studio = useStudioDefinition();
100
+ const fieldEntries = Object.entries(def?.fields ?? {});
88
101
  const config = useBlockConfig(block.type);
89
102
  const Preview = config?.preview;
90
103
 
@@ -140,41 +153,27 @@ function BlockItem({
140
153
  <Preview type={block.type} props={block.props} />
141
154
  </div>
142
155
  ) : null}
143
- {fields.length === 0 ? (
156
+ {fieldEntries.length === 0 ? (
144
157
  <Text tone="muted" size="sm">
145
158
  {def
146
159
  ? "This block has no fields."
147
160
  : `Unknown block type "${block.type}".`}
148
161
  </Text>
149
162
  ) : (
150
- fields.map((name) => {
151
- const field = def?.fields[name];
152
- const CustomInput = resolveFieldInput(studio, field);
153
- return (
154
- <Field key={name} label={fieldLabel(name)}>
155
- {CustomInput && field ? (
156
- <CustomInput
157
- name={name}
158
- field={field}
159
- value={block.props[name]}
160
- readOnly={readOnly}
161
- onChange={(next) => onUpdateProps({ ...block.props, [name]: next })}
162
- />
163
- ) : (
164
- <Input
165
- value={String(block.props[name] ?? "")}
166
- readOnly={readOnly}
167
- onChange={(e) =>
168
- onUpdateProps({
169
- ...block.props,
170
- [name]: e.target.value,
171
- })
172
- }
173
- />
174
- )}
175
- </Field>
176
- );
177
- })
163
+ fieldEntries.map(([name, field]) => (
164
+ <Field key={name} label={fieldLabel(name, field)}>
165
+ <FieldControl
166
+ name={name}
167
+ field={field}
168
+ value={block.props[name]}
169
+ models={models}
170
+ objects={objects}
171
+ documentLists={documentLists}
172
+ readOnly={readOnly}
173
+ onChange={(next) => onUpdateProps(omitField(block.props, name, next))}
174
+ />
175
+ </Field>
176
+ ))
178
177
  )}
179
178
  </div>
180
179
  ) : null}
@@ -185,12 +184,18 @@ function BlockItem({
185
184
  export function BlocksList({
186
185
  blocks,
187
186
  catalog,
187
+ models,
188
+ objects = [],
189
+ documentLists,
188
190
  readOnly,
189
191
  focusBlock,
190
192
  onChange,
191
193
  }: {
192
194
  blocks: BlockInstance[];
193
195
  catalog: BlockSnapshot[];
196
+ models: ModelRef[];
197
+ objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
198
+ documentLists?: RelationDocumentLists;
194
199
  readOnly?: boolean;
195
200
  focusBlock?: { id: string; nonce: number } | null;
196
201
  onChange: (next: BlockInstance[]) => void;
@@ -241,6 +246,9 @@ export function BlocksList({
241
246
  block={block}
242
247
  index={index}
243
248
  catalog={catalog}
249
+ models={models}
250
+ objects={objects}
251
+ documentLists={documentLists}
244
252
  open={openId === block.id}
245
253
  readOnly={readOnly}
246
254
  onToggle={() => setOpenId(openId === block.id ? null : block.id)}
@@ -2,7 +2,7 @@
2
2
  import { useId, useRef, useState, type KeyboardEvent } from "react";
3
3
  import { Menu } from "@base-ui/react/menu";
4
4
  import * as stylex from "@stylexjs/stylex";
5
- import type { BlockSnapshot } from "../api";
5
+ import type { BlockSnapshot, SchemaField } from "../api";
6
6
  import { colors, radius, space, type } from "../styles/tokens.stylex";
7
7
  import { Button } from "../ui/Button";
8
8
  import { Textarea } from "../ui/Input";
@@ -15,15 +15,27 @@ type BodyView = "rendered" | "source";
15
15
 
16
16
  const hover = "@media (hover: hover)";
17
17
 
18
+ type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
19
+ type RelationDocumentLists = Record<
20
+ string,
21
+ Array<{ slug: string; frontmatter: Record<string, unknown> }>
22
+ >;
23
+
18
24
  export function BodyField({
19
25
  value,
20
26
  catalog = [],
27
+ models,
28
+ objects = [],
29
+ documentLists,
21
30
  readOnly,
22
31
  onChange,
23
32
  onSelect,
24
33
  }: {
25
34
  value: string;
26
35
  catalog?: BlockSnapshot[];
36
+ models: ModelRef[];
37
+ objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
38
+ documentLists?: RelationDocumentLists;
27
39
  readOnly?: boolean;
28
40
  onChange: (value: string) => void;
29
41
  onSelect: (selection: { start: number; end: number } | null) => void;
@@ -153,6 +165,9 @@ export function BodyField({
153
165
  ref={rendered}
154
166
  value={value}
155
167
  catalog={catalog}
168
+ models={models}
169
+ objects={objects}
170
+ documentLists={documentLists}
156
171
  readOnly={readOnly}
157
172
  labelledBy={labelId}
158
173
  onChange={onChange}
@@ -0,0 +1,221 @@
1
+ // packages/studio/src/pages/CodeSessionFiles.tsx
2
+ import { useMemo } from "react";
3
+ import * as stylex from "@stylexjs/stylex";
4
+ import { colors, space, type } from "../styles/tokens.stylex";
5
+ import { Text } from "../ui/Text";
6
+ import { fileTree, type FileNode, type WorkspaceFile } from "./code-session-files";
7
+
8
+ export type FilesView = {
9
+ /** Checkout paths, or null while they load. */
10
+ list: string[] | null;
11
+ error: string | null;
12
+ selected: string | null;
13
+ opened: WorkspaceFile | "loading" | null;
14
+ onSelect: (path: string) => void;
15
+ };
16
+
17
+ export function CodeSessionFiles({ list, error, selected, opened, onSelect }: FilesView) {
18
+ const tree = useMemo(() => fileTree(list ?? []), [list]);
19
+
20
+ return (
21
+ <div {...stylex.props(styles.shell)} aria-label="Session files">
22
+ <nav {...stylex.props(styles.tree)} aria-label="Checkout">
23
+ {error ? (
24
+ <p {...stylex.props(styles.note, styles.error)}>{error}</p>
25
+ ) : list === null ? (
26
+ <p {...stylex.props(styles.note)}>Reading the checkout…</p>
27
+ ) : list.length === 0 ? (
28
+ <p {...stylex.props(styles.note)}>The checkout is empty.</p>
29
+ ) : (
30
+ <TreeLevel nodes={tree} depth={0} selected={selected} onSelect={onSelect} />
31
+ )}
32
+ </nav>
33
+ <section {...stylex.props(styles.viewer)} aria-label="File">
34
+ {selected ? (
35
+ <>
36
+ <header {...stylex.props(styles.viewerHeader)}>
37
+ <span {...stylex.props(styles.path)}>{selected}</span>
38
+ </header>
39
+ <FileBody opened={opened} />
40
+ </>
41
+ ) : (
42
+ <div {...stylex.props(styles.empty)}>
43
+ <Text size="sm" tone="muted">
44
+ Pick a file to read it as the host has it now.
45
+ </Text>
46
+ </div>
47
+ )}
48
+ </section>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function TreeLevel({
54
+ nodes,
55
+ depth,
56
+ selected,
57
+ onSelect,
58
+ }: {
59
+ nodes: FileNode[];
60
+ depth: number;
61
+ selected: string | null;
62
+ onSelect: (path: string) => void;
63
+ }) {
64
+ const indent = { paddingLeft: `${8 + depth * 12}px` };
65
+ return (
66
+ <ul {...stylex.props(styles.level)}>
67
+ {nodes.map((node) =>
68
+ node.kind === "dir" ? (
69
+ <li key={node.path}>
70
+ <details open={selected?.startsWith(`${node.path}/`) ?? false}>
71
+ <summary {...stylex.props(styles.row, styles.dir)} style={indent}>
72
+ {node.name}
73
+ </summary>
74
+ <TreeLevel
75
+ nodes={node.children}
76
+ depth={depth + 1}
77
+ selected={selected}
78
+ onSelect={onSelect}
79
+ />
80
+ </details>
81
+ </li>
82
+ ) : (
83
+ <li key={node.path}>
84
+ <button
85
+ type="button"
86
+ aria-current={selected === node.path ? "true" : undefined}
87
+ onClick={() => onSelect(node.path)}
88
+ {...stylex.props(styles.row, styles.file, selected === node.path && styles.fileActive)}
89
+ style={indent}
90
+ >
91
+ {node.name}
92
+ </button>
93
+ </li>
94
+ ),
95
+ )}
96
+ </ul>
97
+ );
98
+ }
99
+
100
+ function FileBody({ opened }: { opened: WorkspaceFile | "loading" | null }) {
101
+ if (opened === "loading" || opened === null) {
102
+ return <p {...stylex.props(styles.note)}>{opened === "loading" ? "Opening…" : ""}</p>;
103
+ }
104
+ if ("binary" in opened) {
105
+ return <p {...stylex.props(styles.note)}>Binary file, {formatSize(opened.size)}.</p>;
106
+ }
107
+ if ("tooLarge" in opened) {
108
+ return <p {...stylex.props(styles.note)}>Too large to show here ({formatSize(opened.size)}).</p>;
109
+ }
110
+ return <pre {...stylex.props(styles.code)}>{opened.content}</pre>;
111
+ }
112
+
113
+ function formatSize(bytes: number): string {
114
+ if (bytes < 1024) return `${bytes} B`;
115
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
116
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
117
+ }
118
+
119
+ const hover = "@media (hover: hover)";
120
+
121
+ const styles = stylex.create({
122
+ shell: {
123
+ display: "flex",
124
+ flex: 1,
125
+ minWidth: 0,
126
+ minHeight: 0,
127
+ },
128
+ tree: {
129
+ width: "260px",
130
+ flexShrink: 0,
131
+ overflow: "auto",
132
+ paddingBlock: space.sm,
133
+ borderRightWidth: 1,
134
+ borderRightStyle: "solid",
135
+ borderRightColor: colors.border,
136
+ backgroundColor: colors.bgSurface,
137
+ },
138
+ level: {
139
+ margin: 0,
140
+ padding: 0,
141
+ listStyle: "none",
142
+ },
143
+ row: {
144
+ display: "block",
145
+ width: "100%",
146
+ boxSizing: "border-box",
147
+ paddingBlock: "3px",
148
+ paddingRight: space.sm,
149
+ font: "inherit",
150
+ fontSize: type.xs,
151
+ lineHeight: 1.5,
152
+ textAlign: "left",
153
+ whiteSpace: "nowrap",
154
+ overflow: "hidden",
155
+ textOverflow: "ellipsis",
156
+ cursor: "pointer",
157
+ borderWidth: 0,
158
+ color: colors.text,
159
+ backgroundColor: {
160
+ default: "transparent",
161
+ [hover]: { default: null, ":hover": colors.bgHover },
162
+ },
163
+ },
164
+ dir: {
165
+ fontWeight: 500,
166
+ },
167
+ file: {
168
+ color: colors.textMuted,
169
+ },
170
+ fileActive: {
171
+ color: colors.text,
172
+ backgroundColor: colors.bgActive,
173
+ },
174
+ viewer: {
175
+ display: "flex",
176
+ flex: 1,
177
+ flexDirection: "column",
178
+ minWidth: 0,
179
+ minHeight: 0,
180
+ backgroundColor: colors.bgApp,
181
+ },
182
+ viewerHeader: {
183
+ paddingBlock: space.sm,
184
+ paddingInline: space.lg,
185
+ borderBottomWidth: 1,
186
+ borderBottomStyle: "solid",
187
+ borderBottomColor: colors.border,
188
+ },
189
+ path: {
190
+ fontFamily: type.mono,
191
+ fontSize: type.xs,
192
+ color: colors.textMuted,
193
+ },
194
+ code: {
195
+ flex: 1,
196
+ margin: 0,
197
+ overflow: "auto",
198
+ padding: space.lg,
199
+ fontFamily: type.mono,
200
+ fontSize: "12px",
201
+ lineHeight: 1.6,
202
+ tabSize: 2,
203
+ color: colors.text,
204
+ },
205
+ empty: {
206
+ display: "flex",
207
+ flex: 1,
208
+ alignItems: "center",
209
+ justifyContent: "center",
210
+ },
211
+ note: {
212
+ margin: 0,
213
+ paddingBlock: space.sm,
214
+ paddingInline: space.lg,
215
+ fontSize: type.xs,
216
+ color: colors.textMuted,
217
+ },
218
+ error: {
219
+ color: colors.danger,
220
+ },
221
+ });