@backmatter/studio 0.0.16 → 0.0.18

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 (45) hide show
  1. package/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-DkuQBiQZ.js} +2 -2
  2. package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DVvb3N-V.js} +27489 -26622
  3. package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-B4FITg6c.js} +2 -2
  4. package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
  5. package/dist/chunks/{index-D_BHF_8q.js → index-CegtXnEI.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 +45 -1
  10. package/dist/site-widget.js +2799 -2791
  11. package/dist/studio.css +113 -15
  12. package/package.json +2 -2
  13. package/src/api.ts +22 -0
  14. package/src/chat/ChatProvider.tsx +6 -0
  15. package/src/chat/Composer.tsx +3 -1
  16. package/src/chrome/Toast.tsx +115 -0
  17. package/src/chrome/WorkspaceStatusBar.tsx +12 -3
  18. package/src/chrome/commit-error.ts +13 -0
  19. package/src/chrome/pull-failure.ts +61 -0
  20. package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
  21. package/src/fields/FieldControl.tsx +771 -0
  22. package/src/fields/LinkFieldInput.tsx +315 -0
  23. package/src/fields/choose-field-input.ts +69 -0
  24. package/src/fields/link-targets.ts +60 -0
  25. package/src/fields/load-relation-documents.ts +35 -0
  26. package/src/fields/omit-field.ts +12 -0
  27. package/src/fields/relation-targets.ts +44 -0
  28. package/src/fields/resolve-object-fields.ts +21 -0
  29. package/src/host-path.ts +13 -0
  30. package/src/pages/BlockSettingsDialog.tsx +19 -115
  31. package/src/pages/BlocksList.tsx +43 -35
  32. package/src/pages/BodyProse.tsx +16 -1
  33. package/src/pages/CodeSessionPage.tsx +186 -248
  34. package/src/pages/DocEditorPage.tsx +168 -213
  35. package/src/pages/MediaFieldInput.tsx +609 -26
  36. package/src/pages/MediaGalleryPage.tsx +14 -3
  37. package/src/pages/PreviewAddress.tsx +263 -0
  38. package/src/pages/RenderedBody.tsx +17 -2
  39. package/src/pages/branch-notice.ts +16 -0
  40. package/src/pages/code-session-boot.ts +0 -12
  41. package/src/pages/code-session-context.tsx +2 -0
  42. package/src/pages/media-selection.ts +79 -0
  43. package/src/shell-resets.css +15 -0
  44. package/src/ui/Input.tsx +178 -20
  45. package/src/ui/popup.ts +11 -2
@@ -3,30 +3,32 @@ import { useState } from "react";
3
3
  import { Dialog } from "@base-ui/react/dialog";
4
4
  import * as stylex from "@stylexjs/stylex";
5
5
  import type { SchemaField } from "../api";
6
- import { useFieldInput } from "../define-studio";
6
+ import { FieldControl } from "../fields/FieldControl";
7
+ import { omitField } from "../fields/omit-field";
7
8
  import { colors, radius, space, type, z } from "../styles/tokens.stylex";
8
9
  import { Button } from "../ui/Button";
9
10
  import { Field } from "../ui/Field";
10
- import { Input, Textarea } from "../ui/Input";
11
11
  import { cx, isTransitioning } from "../ui/popup";
12
- import { MediaFieldInput } from "./MediaFieldInput";
12
+
13
+ type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
14
+ type RelationDocumentLists = Record<
15
+ string,
16
+ Array<{ slug: string; frontmatter: Record<string, unknown> }>
17
+ >;
13
18
 
14
19
  function fieldLabel(name: string, field: SchemaField) {
15
20
  if ("label" in field && typeof field.label === "string" && field.label) return field.label;
16
21
  return name.charAt(0).toUpperCase() + name.slice(1);
17
22
  }
18
23
 
19
- function compositeText(value: unknown): string {
20
- if (value == null || value === "") return "";
21
- if (typeof value === "string") return value;
22
- return JSON.stringify(value, null, 2);
23
- }
24
-
25
24
  export function BlockSettingsDialog({
26
25
  name,
27
26
  title,
28
27
  fields,
29
28
  value,
29
+ models,
30
+ objects = [],
31
+ documentLists,
30
32
  readOnly,
31
33
  onOpenChange,
32
34
  onSave,
@@ -36,32 +38,19 @@ export function BlockSettingsDialog({
36
38
  title?: string;
37
39
  fields: Record<string, SchemaField>;
38
40
  value: Record<string, unknown>;
41
+ models: ModelRef[];
42
+ objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
43
+ documentLists?: RelationDocumentLists;
39
44
  readOnly?: boolean;
40
45
  onOpenChange: (open: boolean) => void;
41
46
  onSave: (props: Record<string, unknown>) => void;
42
47
  onRemove: () => void;
43
48
  }) {
44
49
  const [draft, setDraft] = useState(value);
45
- const [error, setError] = useState<string | null>(null);
46
50
  const names = Object.keys(fields);
47
51
 
48
52
  function save() {
49
- const next = { ...draft };
50
- for (const [fieldName, field] of Object.entries(fields)) {
51
- if (field.kind !== "object" && field.kind !== "array") continue;
52
- const raw = next[fieldName];
53
- if (typeof raw !== "string" || raw.trim() === "") {
54
- next[fieldName] = raw === "" ? "" : raw;
55
- continue;
56
- }
57
- try {
58
- next[fieldName] = JSON.parse(raw);
59
- } catch {
60
- setError(`${fieldLabel(fieldName, field)} needs valid JSON.`);
61
- return;
62
- }
63
- }
64
- onSave(next);
53
+ onSave(draft);
65
54
  }
66
55
 
67
56
  return (
@@ -100,17 +89,18 @@ export function BlockSettingsDialog({
100
89
  name={fieldName}
101
90
  field={field}
102
91
  value={draft[fieldName]}
92
+ models={models}
93
+ objects={objects}
94
+ documentLists={documentLists}
103
95
  readOnly={readOnly}
104
96
  onChange={(next) => {
105
- setError(null);
106
- setDraft((current) => ({ ...current, [fieldName]: next }));
97
+ setDraft((current) => omitField(current, fieldName, next));
107
98
  }}
108
99
  />
109
100
  </Field>
110
101
  );
111
102
  })
112
103
  )}
113
- {error ? <p {...stylex.props(styles.error)}>{error}</p> : null}
114
104
  <div {...stylex.props(styles.actions)}>
115
105
  {readOnly ? null : (
116
106
  <Button variant="danger" onClick={onRemove}>
@@ -137,82 +127,6 @@ export function BlockSettingsDialog({
137
127
  );
138
128
  }
139
129
 
140
- function FieldControl({
141
- name,
142
- field,
143
- value,
144
- readOnly,
145
- onChange,
146
- }: {
147
- name: string;
148
- field: SchemaField;
149
- value: unknown;
150
- readOnly?: boolean;
151
- onChange: (value: unknown) => void;
152
- }) {
153
- const CustomInput = useFieldInput(field);
154
- if (CustomInput) {
155
- return (
156
- <CustomInput name={name} field={field} value={value} readOnly={readOnly} onChange={onChange} />
157
- );
158
- }
159
- if (field.kind === "media") {
160
- return <MediaFieldInput name={name} value={value} readOnly={readOnly} onChange={onChange} />;
161
- }
162
- if (field.kind === "boolean") {
163
- return (
164
- <input
165
- type="checkbox"
166
- checked={value === true}
167
- disabled={readOnly}
168
- onChange={(event) => onChange(event.target.checked)}
169
- {...stylex.props(styles.check)}
170
- />
171
- );
172
- }
173
- if (field.kind === "number") {
174
- return (
175
- <Input
176
- type="number"
177
- value={value == null || value === "" ? "" : String(value)}
178
- readOnly={readOnly}
179
- onChange={(event) => {
180
- const raw = event.target.value;
181
- onChange(raw === "" ? "" : Number(raw));
182
- }}
183
- />
184
- );
185
- }
186
- if ("multiline" in field && field.multiline) {
187
- return (
188
- <Textarea
189
- rows={4}
190
- value={value == null ? "" : String(value)}
191
- readOnly={readOnly}
192
- onChange={(event) => onChange(event.target.value)}
193
- />
194
- );
195
- }
196
- if (field.kind === "object" || field.kind === "array") {
197
- return (
198
- <Textarea
199
- rows={4}
200
- value={compositeText(value)}
201
- readOnly={readOnly}
202
- spellCheck={false}
203
- onChange={(event) => onChange(event.target.value)}
204
- />
205
- );
206
- }
207
- return (
208
- <Input
209
- value={value == null ? "" : String(value)}
210
- readOnly={readOnly}
211
- onChange={(event) => onChange(event.target.value)}
212
- />
213
- );
214
- }
215
-
216
130
  const styles = stylex.create({
217
131
  backdrop: {
218
132
  position: "fixed",
@@ -282,16 +196,6 @@ const styles = stylex.create({
282
196
  color: colors.textMuted,
283
197
  fontSize: type.sm,
284
198
  },
285
- error: {
286
- margin: 0,
287
- color: colors.danger,
288
- fontSize: type.sm,
289
- },
290
- check: {
291
- width: 16,
292
- height: 16,
293
- margin: 0,
294
- },
295
199
  actions: {
296
200
  display: "flex",
297
201
  alignItems: "center",
@@ -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}