@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,8 +3,10 @@ import { Snake } from "loading-dev";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useOutletContext, useParams, useSearchParams, Link, useNavigate } from "react-router";
5
5
  import * as stylex from "@stylexjs/stylex";
6
+ import { matchRoute, repoPathFromModel } from "@backmatter/core";
6
7
  import {
7
8
  fetchDoc,
9
+ fetchDocs,
8
10
  notifyTreeChanged,
9
11
  type TreeChangeDetail,
10
12
  createPreviewTicket,
@@ -17,8 +19,12 @@ import {
17
19
  type SchemaPayload,
18
20
  type SchemaField,
19
21
  } from "../api";
20
- import { resolveFieldInput, useStudioDefinition } from "../define-studio";
21
- import { MediaFieldInput } from "./MediaFieldInput";
22
+ import { FieldControl } from "../fields/FieldControl";
23
+ import { loadRelationDocuments } from "../fields/load-relation-documents";
24
+ import { omitField } from "../fields/omit-field";
25
+ import { linkDocumentTypes } from "../fields/link-targets";
26
+ import { relationTargets } from "../fields/relation-targets";
27
+ import { useReplicaState } from "../replica/useReplica";
22
28
  import { useSetEditorActions } from "../chrome/EditorActionsContext";
23
29
  import {
24
30
  DOC_CHANGED_EVENT,
@@ -30,7 +36,6 @@ import {
30
36
  } from "../chrome/EditorFocusContext";
31
37
  import { useSetEditorStatus } from "../chrome/EditorStatusContext";
32
38
  import { modifierSymbol } from "../chrome/hotkeys";
33
- import { ReloadIcon } from "../chrome/icons";
34
39
  import { colors, radius, space, type } from "../styles/tokens.stylex";
35
40
  import { Button, ButtonLink } from "../ui/Button";
36
41
  import { ConfirmDialog } from "../ui/ConfirmDialog";
@@ -46,6 +51,8 @@ import {
46
51
  persistPreviewPct,
47
52
  readPreviewPct,
48
53
  } from "./preview-split";
54
+ import { routesForContentFiles } from "../content-routes";
55
+ import { PreviewAddress } from "./PreviewAddress";
49
56
  import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
50
57
  import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
51
58
  import {
@@ -78,10 +85,6 @@ import {
78
85
  const AUTOSAVE_MS = 400;
79
86
  const hover = "@media (hover: hover)";
80
87
 
81
- const spin = stylex.keyframes({
82
- to: { transform: "rotate(360deg)" },
83
- });
84
-
85
88
  const pulse = stylex.keyframes({
86
89
  "0%": { opacity: 0.4 },
87
90
  "50%": { opacity: 1 },
@@ -130,48 +133,6 @@ function emptyFrontmatter(model: SchemaPayload["models"][number] | undefined) {
130
133
  return out;
131
134
  }
132
135
 
133
- function JsonField({
134
- value,
135
- kind,
136
- readOnly,
137
- onValidChange,
138
- }: {
139
- value: unknown;
140
- kind: "object" | "array";
141
- readOnly?: boolean;
142
- onValidChange: (next: unknown) => void;
143
- }) {
144
- const serialized = JSON.stringify(value ?? (kind === "array" ? [] : {}), null, 2);
145
- const [draft, setDraft] = useState(serialized);
146
- const [focused, setFocused] = useState(false);
147
- const [invalid, setInvalid] = useState(false);
148
- useEffect(() => {
149
- if (!focused) {
150
- setDraft(serialized);
151
- setInvalid(false);
152
- }
153
- }, [serialized, focused]);
154
- return (
155
- <Textarea
156
- value={draft}
157
- readOnly={readOnly}
158
- aria-invalid={invalid || undefined}
159
- onFocus={() => setFocused(true)}
160
- onBlur={() => setFocused(false)}
161
- onChange={(e) => {
162
- const next = e.target.value;
163
- setDraft(next);
164
- try {
165
- onValidChange(JSON.parse(next || (kind === "array" ? "[]" : "{}")));
166
- setInvalid(false);
167
- } catch {
168
- setInvalid(true);
169
- }
170
- }}
171
- />
172
- );
173
- }
174
-
175
136
  /** Status-bar label for the doc's state against the branch HEAD. */
176
137
  export function docStatusLabel(status: DocStatus | null): string | null {
177
138
  if (status === null) return null;
@@ -202,7 +163,34 @@ export function DocEditorPage() {
202
163
  const [docFormat, setDocFormat] = useState<"mdx" | "page">("mdx");
203
164
  const [fieldKinds, setFieldKinds] = useState<Record<string, string>>({});
204
165
  const [fieldDefs, setFieldDefs] = useState<Record<string, SchemaField>>({});
205
- const studio = useStudioDefinition();
166
+ const replica = useReplicaState();
167
+ const objects = useMemo(
168
+ () => replica.checkout(workspace.id)?.schema?.payload.objects ?? [],
169
+ [replica, workspace.id],
170
+ );
171
+ const relationTargetTypes = useMemo(() => {
172
+ const targets = new Set<string>();
173
+ const groups: Record<string, SchemaField>[] = [];
174
+ if (model?.fields) groups.push(model.fields);
175
+ for (const block of blocks) {
176
+ if (block.fields) groups.push(block.fields);
177
+ }
178
+ for (const block of richtext) {
179
+ if (block.fields) groups.push(block.fields);
180
+ }
181
+ for (const fields of groups) {
182
+ for (const type of relationTargets(fields, models, objects)) {
183
+ targets.add(type);
184
+ }
185
+ for (const type of linkDocumentTypes(fields, models, objects)) {
186
+ targets.add(type);
187
+ }
188
+ }
189
+ return [...targets].sort();
190
+ }, [model, blocks, richtext, models, objects]);
191
+ const [documentLists, setDocumentLists] = useState<
192
+ Record<string, Array<{ slug: string; frontmatter: Record<string, unknown> }>> | undefined
193
+ >(undefined);
206
194
  const [parseFailed, setParseFailed] = useState(false);
207
195
  const [error, setError] = useState<string | null>(null);
208
196
  const [status, setStatus] = useState<string | null>(null);
@@ -260,6 +248,9 @@ export function DocEditorPage() {
260
248
  editEpochRef.current = editEpoch;
261
249
 
262
250
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
251
+ const docIdentity = `${type}\0${slug}`;
252
+ const [browse, setBrowse] = useState<{ doc: string; path: string } | null>(null);
253
+ const browsedPath = browse?.doc === docIdentity ? browse.path : null;
263
254
  const isPage = docFormat === "page";
264
255
  const detailNames = fieldNames.filter((name) => fieldKinds[name] !== "blocks");
265
256
  const blocksField =
@@ -282,18 +273,28 @@ export function DocEditorPage() {
282
273
  branch,
283
274
  tree,
284
275
  });
276
+ const framePath = browsedPath ?? previewPath;
277
+ // Each document is the content file its model path names. A deleted document is gone.
278
+ const previewRoutes = routesForContentFiles(
279
+ models,
280
+ replica.docs(workspaceId).flatMap((doc) => {
281
+ if (doc.status === "deleted") return [];
282
+ const model = models.find((item) => item.type === doc.type);
283
+ return model ? [repoPathFromModel(model, doc.slug)] : [];
284
+ }),
285
+ );
285
286
 
286
287
  useEffect(() => {
287
- if (!frameIdentity || !previewPath || !workspaceId || !siteUrl) {
288
+ if (!frameIdentity || !framePath || !workspaceId || !siteUrl) {
288
289
  setPreviewUrl(null);
289
290
  return;
290
291
  }
291
292
  if (studioIsOnSite(siteUrl, window.location.origin)) {
292
- setPreviewUrl(onSitePreviewUrl(siteUrl, previewPath));
293
+ setPreviewUrl(onSitePreviewUrl(siteUrl, framePath));
293
294
  return;
294
295
  }
295
296
  let cancelled = false;
296
- void createPreviewTicket(workspaceId, { redirect: previewPath, branch, tree })
297
+ void createPreviewTicket(workspaceId, { redirect: framePath, branch, tree })
297
298
  .then((res) => {
298
299
  if (!cancelled) setPreviewUrl(previewEnableUrl(siteUrl, res.ticket));
299
300
  })
@@ -303,7 +304,32 @@ export function DocEditorPage() {
303
304
  return () => {
304
305
  cancelled = true;
305
306
  };
306
- }, [frameIdentity, previewPath, workspaceId, siteUrl, branch, tree]);
307
+ }, [frameIdentity, framePath, workspaceId, siteUrl, branch, tree]);
308
+
309
+ useEffect(() => {
310
+ if (relationTargetTypes.length === 0) {
311
+ setDocumentLists({});
312
+ return;
313
+ }
314
+ let cancelled = false;
315
+ setDocumentLists(undefined);
316
+ void loadRelationDocuments(
317
+ relationTargetTypes,
318
+ async () => (await fetchDocs(workspaceId)).docs,
319
+ async (relationType, relationSlug) => ({
320
+ frontmatter: (await fetchDoc(workspaceId, relationType, relationSlug)).doc.frontmatter,
321
+ }),
322
+ )
323
+ .then((loaded) => {
324
+ if (!cancelled) setDocumentLists(loaded);
325
+ })
326
+ .catch(() => {
327
+ if (!cancelled) setDocumentLists({});
328
+ });
329
+ return () => {
330
+ cancelled = true;
331
+ };
332
+ }, [workspaceId, type, slug, relationTargetTypes.join("\0")]);
307
333
 
308
334
  useEffect(() => {
309
335
  setPreviewLoading(true);
@@ -477,6 +503,17 @@ export function DocEditorPage() {
477
503
  setPreviewKey((k) => k + 1);
478
504
  }
479
505
 
506
+ function onPreviewPath(next: string) {
507
+ const pathname = next.split(/[?#]/, 1)[0] || "/";
508
+ const doc = matchRoute(models, pathname);
509
+ if (doc && (doc.type !== type || doc.slug !== slug)) {
510
+ navigate(contentDocPath(workspace.slug, doc.type, doc.slug));
511
+ return;
512
+ }
513
+ if (next === framePath) remountPreview();
514
+ setBrowse({ doc: docIdentity, path: next });
515
+ }
516
+
480
517
  function choosePreviewSize(size: PreviewSize) {
481
518
  setPreviewSize(size);
482
519
  persistPreviewSize(size);
@@ -870,32 +907,7 @@ export function DocEditorPage() {
870
907
  style={{ ...previewSx.style, flexBasis: `${previewPct}%` }}
871
908
  >
872
909
  <div {...stylex.props(styles.previewBar)}>
873
- <div {...stylex.props(styles.urlGroup)}>
874
- <Tooltip label={previewLoading ? "Loading preview…" : "Reload preview"}>
875
- <button
876
- type="button"
877
- aria-label={previewLoading ? "Preview loading" : "Reload preview"}
878
- aria-busy={previewLoading || undefined}
879
- onClick={remountPreview}
880
- {...stylex.props(styles.reloadBtn)}
881
- >
882
- <span
883
- {...stylex.props(
884
- styles.reloadIcon,
885
- previewLoading && styles.reloadSpinning,
886
- )}
887
- >
888
- <ReloadIcon />
889
- </span>
890
- </button>
891
- </Tooltip>
892
- <span {...stylex.props(styles.url)} title={previewUrl ?? undefined}>
893
- {previewUrl
894
- ? previewUrl.replace(/^https?:\/\//, "")
895
- : !siteUrl
896
- ? "No site URL"
897
- : "Loading checkout…"}
898
- </span>
910
+ <div {...stylex.props(styles.previewLead)}>
899
911
  {previewLive === "rendered" ? (
900
912
  <span {...stylex.props(styles.previewLive)}>Updated</span>
901
913
  ) : null}
@@ -916,15 +928,20 @@ export function DocEditorPage() {
916
928
  </button>
917
929
  ) : null}
918
930
  </div>
919
- <PreviewSizeSwitch size={previewSize} onSize={choosePreviewSize} />
920
- <span
921
- {...stylex.props(
922
- styles.badge,
923
- inConflict && styles.badgeConflict,
924
- )}
925
- >
926
- {statusLabel ?? "…"}
927
- </span>
931
+ <PreviewAddress
932
+ path={framePath ?? (!siteUrl ? "No site URL" : "Loading checkout…")}
933
+ enabled={previewUrl !== null}
934
+ routes={previewRoutes}
935
+ routesPending={!replica.ready}
936
+ onPath={onPreviewPath}
937
+ onReload={remountPreview}
938
+ onOpen={() => {
939
+ if (previewUrl) window.open(previewUrl, "_blank", "noopener,noreferrer");
940
+ }}
941
+ />
942
+ <div {...stylex.props(styles.sizeSwitch)}>
943
+ <PreviewSizeSwitch size={previewSize} onSize={choosePreviewSize} />
944
+ </div>
928
945
  </div>
929
946
  {previewUrl ? (
930
947
  <PreviewViewport size={previewSize} dragging={dragging}>
@@ -989,7 +1006,7 @@ export function DocEditorPage() {
989
1006
  <div {...stylex.props(styles.editorColumn, !showPreview && styles.editorColumnSolo)}>
990
1007
  <div {...stylex.props(styles.toolbar)}>
991
1008
  <div {...stylex.props(styles.meta)}>
992
- <Text as="h1" size="md" weight="semibold" truncate>
1009
+ <Text as="h1" size="sm" weight="medium" truncate>
993
1010
  {type}/{slug}
994
1011
  </Text>
995
1012
  </div>
@@ -1103,55 +1120,33 @@ export function DocEditorPage() {
1103
1120
  </Text>
1104
1121
  ) : (
1105
1122
  detailNames.map((name) => {
1106
- const kind = fieldKinds[name];
1107
- const composite = kind === "object" || kind === "array";
1108
- const CustomInput = resolveFieldInput(studio, fieldDefs[name]);
1123
+ const field = fieldDefs[name]!;
1109
1124
  return (
1110
1125
  <Field key={name} label={fieldLabel(name)}>
1111
- {CustomInput ? (
1112
- <CustomInput
1113
- name={name}
1114
- field={fieldDefs[name]!}
1115
- value={frontmatter[name]}
1116
- readOnly={aiEditing || parseFailed}
1117
- onChange={(next) => {
1118
- markEdited();
1119
- setFrontmatter((prev) => ({ ...prev, [name]: next }));
1120
- }}
1121
- />
1122
- ) : kind === "media" ? (
1123
- <MediaFieldInput
1124
- name={name}
1125
- value={frontmatter[name]}
1126
- readOnly={aiEditing || parseFailed}
1127
- onChange={(next) => {
1128
- markEdited();
1129
- setFrontmatter((prev) => ({ ...prev, [name]: next }));
1130
- }}
1131
- />
1132
- ) : composite ? (
1133
- <JsonField
1134
- kind={kind === "array" ? "array" : "object"}
1135
- value={frontmatter[name]}
1136
- readOnly={aiEditing || parseFailed}
1137
- onValidChange={(next) => {
1138
- markEdited();
1139
- setFrontmatter((prev) => ({ ...prev, [name]: next }));
1140
- }}
1141
- />
1142
- ) : (
1143
- <Input
1144
- value={String(frontmatter[name] ?? "")}
1145
- readOnly={aiEditing || parseFailed}
1146
- onChange={(e) => {
1147
- markEdited();
1148
- setFrontmatter((prev) => ({
1149
- ...prev,
1150
- [name]: e.target.value,
1151
- }));
1152
- }}
1153
- />
1154
- )}
1126
+ <FieldControl
1127
+ name={name}
1128
+ field={field}
1129
+ value={frontmatter[name]}
1130
+ models={models}
1131
+ objects={objects}
1132
+ documentLists={documentLists}
1133
+ readOnly={aiEditing || parseFailed}
1134
+ blocksEditor={({ value, readOnly: blocksReadOnly, onChange: onBlocksChange }) => (
1135
+ <BlocksList
1136
+ blocks={blocksFromFrontmatter(value)}
1137
+ catalog={blocks}
1138
+ models={models}
1139
+ objects={objects}
1140
+ documentLists={documentLists}
1141
+ readOnly={blocksReadOnly}
1142
+ onChange={onBlocksChange}
1143
+ />
1144
+ )}
1145
+ onChange={(next) => {
1146
+ markEdited();
1147
+ setFrontmatter((prev) => omitField(prev, name, next));
1148
+ }}
1149
+ />
1155
1150
  </Field>
1156
1151
  );
1157
1152
  })
@@ -1160,6 +1155,9 @@ export function DocEditorPage() {
1160
1155
  <BlocksList
1161
1156
  blocks={blocksFromFrontmatter(frontmatter[blocksField])}
1162
1157
  catalog={blocks}
1158
+ models={models}
1159
+ objects={objects}
1160
+ documentLists={documentLists}
1163
1161
  readOnly={aiEditing || parseFailed}
1164
1162
  focusBlock={blockFocus}
1165
1163
  onChange={(next) => {
@@ -1171,6 +1169,9 @@ export function DocEditorPage() {
1171
1169
  <BodyField
1172
1170
  value={body}
1173
1171
  catalog={richtext}
1172
+ models={models}
1173
+ objects={objects}
1174
+ documentLists={documentLists}
1174
1175
  readOnly={aiEditing || parseFailed}
1175
1176
  onChange={(value) => {
1176
1177
  markEdited();
@@ -1235,74 +1236,34 @@ const styles = stylex.create({
1235
1236
  flexShrink: 0,
1236
1237
  flexDirection: "column",
1237
1238
  overflow: "hidden",
1238
- backgroundColor: colors.bgSurface,
1239
+ backgroundColor: colors.bgApp,
1239
1240
  },
1240
1241
  previewBar: {
1241
- display: "flex",
1242
- flexShrink: 0,
1242
+ display: "grid",
1243
+ gridTemplateColumns: "1fr minmax(0, 520px) 1fr",
1243
1244
  alignItems: "center",
1244
- justifyContent: "space-between",
1245
+ flexShrink: 0,
1245
1246
  gap: space.md,
1247
+ height: "44px",
1248
+ boxSizing: "border-box",
1246
1249
  paddingInline: space.md,
1247
- paddingBlock: space.sm,
1248
1250
  borderBottomWidth: 1,
1249
1251
  borderBottomStyle: "solid",
1250
1252
  borderBottomColor: colors.border,
1253
+ backgroundColor: colors.bgSurface,
1251
1254
  fontSize: type.xs,
1252
1255
  color: colors.textMuted,
1253
1256
  },
1254
- urlGroup: {
1257
+ previewLead: {
1255
1258
  display: "flex",
1256
- minWidth: 0,
1257
- flex: 1,
1258
1259
  alignItems: "center",
1259
1260
  gap: space.sm,
1260
- },
1261
- reloadBtn: {
1262
- display: "inline-flex",
1263
- flexShrink: 0,
1264
- alignItems: "center",
1265
- justifyContent: "center",
1266
- width: 22,
1267
- height: 22,
1268
- padding: 0,
1269
- borderWidth: 0,
1270
- borderRadius: radius.sm,
1271
- backgroundColor: "transparent",
1272
- color: colors.textMuted,
1273
- cursor: "pointer",
1274
- touchAction: "manipulation",
1275
- [hover]: {
1276
- ":hover": {
1277
- backgroundColor: colors.bgHover,
1278
- color: colors.text,
1279
- },
1280
- },
1281
- ":focus-visible": {
1282
- outlineWidth: 2,
1283
- outlineStyle: "solid",
1284
- outlineColor: colors.focus,
1285
- outlineOffset: 1,
1286
- },
1287
- },
1288
- reloadIcon: {
1289
- display: "inline-flex",
1290
- },
1291
- reloadSpinning: {
1292
- animationName: spin,
1293
- animationDuration: "700ms",
1294
- animationTimingFunction: "linear",
1295
- animationIterationCount: "infinite",
1296
- "@media (prefers-reduced-motion: reduce)": {
1297
- animationName: "none",
1298
- animationDuration: "0s",
1299
- },
1300
- },
1301
- url: {
1261
+ minWidth: 0,
1262
+ justifySelf: "start",
1302
1263
  overflow: "hidden",
1303
- textOverflow: "ellipsis",
1304
- whiteSpace: "nowrap",
1305
- fontFamily: type.mono,
1264
+ },
1265
+ sizeSwitch: {
1266
+ justifySelf: "end",
1306
1267
  },
1307
1268
  previewLive: {
1308
1269
  flexShrink: 0,
@@ -1315,28 +1276,21 @@ const styles = stylex.create({
1315
1276
  editPage: {
1316
1277
  flexShrink: 0,
1317
1278
  borderWidth: 0,
1318
- borderRadius: 4,
1279
+ borderRadius: radius.sm,
1319
1280
  backgroundColor: colors.bgHover,
1320
1281
  color: colors.text,
1321
1282
  cursor: "pointer",
1322
- fontSize: type.sm,
1323
- paddingInline: space.sm,
1324
- paddingBlock: 2,
1325
- },
1326
- badge: {
1327
- flexShrink: 0,
1328
- borderWidth: 1,
1329
- borderStyle: "solid",
1330
- borderColor: colors.border,
1331
- borderRadius: 4,
1283
+ font: "inherit",
1284
+ fontSize: type.xs,
1285
+ fontWeight: 500,
1332
1286
  paddingInline: space.sm,
1333
- paddingBlock: 2,
1334
- textTransform: "uppercase",
1335
- letterSpacing: "0.04em",
1336
- },
1337
- badgeConflict: {
1338
- borderColor: colors.danger,
1339
- color: colors.danger,
1287
+ paddingBlock: "2px",
1288
+ ":focus-visible": {
1289
+ outlineWidth: 2,
1290
+ outlineStyle: "solid",
1291
+ outlineColor: colors.focus,
1292
+ outlineOffset: 1,
1293
+ },
1340
1294
  },
1341
1295
  conflictBanner: {
1342
1296
  display: "flex",
@@ -1459,22 +1413,23 @@ const styles = stylex.create({
1459
1413
  toolbar: {
1460
1414
  display: "flex",
1461
1415
  flexShrink: 0,
1462
- flexWrap: "wrap",
1463
1416
  alignItems: "center",
1464
1417
  justifyContent: "space-between",
1465
- gap: space.sm,
1466
- paddingInline: space.lg,
1467
- paddingBlock: space.sm,
1418
+ gap: space.md,
1419
+ height: "44px",
1420
+ boxSizing: "border-box",
1421
+ paddingInline: space.md,
1468
1422
  borderBottomWidth: 1,
1469
1423
  borderBottomStyle: "solid",
1470
1424
  borderBottomColor: colors.border,
1425
+ backgroundColor: colors.bgSurface,
1471
1426
  },
1472
1427
  meta: {
1473
1428
  minWidth: 0,
1474
1429
  },
1475
1430
  actions: {
1476
1431
  display: "flex",
1477
- flexWrap: "wrap",
1432
+ flexShrink: 0,
1478
1433
  gap: space.sm,
1479
1434
  },
1480
1435
  formWrap: {