@backmatter/studio 0.0.7 → 0.0.8

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.
@@ -1,5 +1,6 @@
1
1
  // packages/studio/src/chrome/ContentExplorerSidebar.tsx
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
3
4
  import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
4
5
  import * as stylex from "@stylexjs/stylex";
5
6
  import { notifyTreeChanged, saveDoc, type DocListItem, type DocStatus } from "../api";
@@ -8,14 +9,14 @@ import { colors, radius, space, type } from "../styles/tokens.stylex";
8
9
  import { Tooltip } from "../ui/Tooltip";
9
10
  import {
10
11
  contentDocPath,
11
- contentTypePath,
12
+ contentRailHref,
12
13
  isWorkspaceContentPath,
13
14
  isWorkspaceMediaPath,
14
15
  isWorkspaceSettingsPath,
15
16
  mediaPath,
16
17
  settingsPath,
17
- workspaceHomePath,
18
18
  } from "../workspace-path";
19
+ import { UserMenu } from "./UserMenu";
19
20
  import {
20
21
  groupContentTypes,
21
22
  type ContentTypeNav,
@@ -62,7 +63,36 @@ const TREE_PAD_PX = 8;
62
63
  const INDENT_PX = 8;
63
64
  const TWISTIE_PX = 16;
64
65
 
65
- export function ContentExplorerSidebar({ workspace }: { workspace: WorkspaceNav }) {
66
+ export function WorkspaceRail({
67
+ workspace,
68
+ treeHost,
69
+ }: {
70
+ workspace: WorkspaceNav | null;
71
+ treeHost: HTMLElement | null;
72
+ }) {
73
+ if (!workspace) {
74
+ return (
75
+ <RailColumn>
76
+ <div {...stylex.props(styles.rail, styles.railAccountOnly)}>
77
+ <UserMenu compact />
78
+ </div>
79
+ </RailColumn>
80
+ );
81
+ }
82
+ return <ContentExplorerSidebar workspace={workspace} treeHost={treeHost} />;
83
+ }
84
+
85
+ function RailColumn({ children }: { children?: React.ReactNode }) {
86
+ return <aside {...stylex.props(styles.railColumn)}>{children}</aside>;
87
+ }
88
+
89
+ function ContentExplorerSidebar({
90
+ workspace,
91
+ treeHost,
92
+ }: {
93
+ workspace: WorkspaceNav;
94
+ treeHost: HTMLElement | null;
95
+ }) {
66
96
  const location = useLocation();
67
97
  const [searchParams] = useSearchParams();
68
98
  const replica = useReplicaState();
@@ -128,80 +158,83 @@ export function ContentExplorerSidebar({ workspace }: { workspace: WorkspaceNav
128
158
 
129
159
  const empty = workspace.contentTypes.length === 0;
130
160
  const settingsActive = isWorkspaceSettingsPath(location.pathname);
131
- const showTree = !isWorkspaceMediaPath(location.pathname);
161
+ const showTree = isWorkspaceContentPath(location.pathname);
132
162
  const canWrite = canWriteContent(workspace.role);
133
163
 
164
+ const tree = showTree ? (
165
+ <div {...stylex.props(styles.panel)}>
166
+ <header {...stylex.props(styles.panelHeader)}>
167
+ <span {...stylex.props(styles.panelTitle)}>Content</span>
168
+ <div {...stylex.props(styles.panelActions)}>
169
+ <IconButton label="New file (soon)" disabled>
170
+ <ExplorerHeaderNewFileIcon />
171
+ </IconButton>
172
+ <IconButton label="View options (soon)" disabled>
173
+ <ExplorerHeaderListIcon />
174
+ </IconButton>
175
+ </div>
176
+ </header>
177
+ <div {...stylex.props(styles.treeScroll)}>
178
+ {empty ? (
179
+ <p {...stylex.props(styles.emptyHint)}>
180
+ No content types yet. Pull from GitHub to load models.
181
+ </p>
182
+ ) : (
183
+ <>
184
+ {grouped.pages.length > 0 ? (
185
+ <ExplorerSection
186
+ section="pages"
187
+ models={grouped.pages}
188
+ workspace={workspace}
189
+ docsByType={docsByType}
190
+ route={route}
191
+ expanded={expanded}
192
+ onToggleFolder={toggleFolder}
193
+ onExpandFolderPath={expandFolderPath}
194
+ canWrite={canWrite}
195
+ />
196
+ ) : null}
197
+ {grouped.data.length > 0 ? (
198
+ <ExplorerSection
199
+ section="data"
200
+ models={grouped.data}
201
+ workspace={workspace}
202
+ docsByType={docsByType}
203
+ route={route}
204
+ expanded={expanded}
205
+ onToggleFolder={toggleFolder}
206
+ onExpandFolderPath={expandFolderPath}
207
+ canWrite={canWrite}
208
+ />
209
+ ) : null}
210
+ {grouped.globals.length > 0 ? (
211
+ <ExplorerSection
212
+ section="globals"
213
+ models={grouped.globals}
214
+ workspace={workspace}
215
+ docsByType={docsByType}
216
+ route={route}
217
+ expanded={expanded}
218
+ onToggleFolder={toggleFolder}
219
+ onExpandFolderPath={expandFolderPath}
220
+ canWrite={canWrite}
221
+ />
222
+ ) : null}
223
+ </>
224
+ )}
225
+ </div>
226
+ </div>
227
+ ) : null;
228
+
134
229
  return (
135
- <div {...stylex.props(styles.root)}>
230
+ <RailColumn>
136
231
  <ActivityRail
137
232
  pendingCount={pendingCount}
138
233
  settingsActive={settingsActive}
139
234
  workspaceSlug={workspace.workspaceSlug}
140
- contentRoute={route}
141
235
  />
142
- {showTree ? <div {...stylex.props(styles.panel)}>
143
- <header {...stylex.props(styles.panelHeader)}>
144
- <span {...stylex.props(styles.panelTitle)}>Content</span>
145
- <div {...stylex.props(styles.panelActions)}>
146
- <IconButton label="New file (soon)" disabled>
147
- <ExplorerHeaderNewFileIcon />
148
- </IconButton>
149
- <IconButton label="View options (soon)" disabled>
150
- <ExplorerHeaderListIcon />
151
- </IconButton>
152
- </div>
153
- </header>
154
- <div {...stylex.props(styles.treeScroll)}>
155
- {empty ? (
156
- <p {...stylex.props(styles.emptyHint)}>
157
- No content types yet. Pull from GitHub to load models.
158
- </p>
159
- ) : (
160
- <>
161
- {grouped.pages.length > 0 ? (
162
- <ExplorerSection
163
- section="pages"
164
- models={grouped.pages}
165
- workspace={workspace}
166
- docsByType={docsByType}
167
- route={route}
168
- expanded={expanded}
169
- onToggleFolder={toggleFolder}
170
- onExpandFolderPath={expandFolderPath}
171
- canWrite={canWrite}
172
- />
173
- ) : null}
174
- {grouped.data.length > 0 ? (
175
- <ExplorerSection
176
- section="data"
177
- models={grouped.data}
178
- workspace={workspace}
179
- docsByType={docsByType}
180
- route={route}
181
- expanded={expanded}
182
- onToggleFolder={toggleFolder}
183
- onExpandFolderPath={expandFolderPath}
184
- canWrite={canWrite}
185
- />
186
- ) : null}
187
- {grouped.globals.length > 0 ? (
188
- <ExplorerSection
189
- section="globals"
190
- models={grouped.globals}
191
- workspace={workspace}
192
- docsByType={docsByType}
193
- route={route}
194
- expanded={expanded}
195
- onToggleFolder={toggleFolder}
196
- onExpandFolderPath={expandFolderPath}
197
- canWrite={canWrite}
198
- />
199
- ) : null}
200
- </>
201
- )}
202
- </div>
203
- </div> : null}
204
- </div>
236
+ {tree && treeHost ? createPortal(tree, treeHost) : null}
237
+ </RailColumn>
205
238
  );
206
239
  }
207
240
 
@@ -209,12 +242,10 @@ function ActivityRail({
209
242
  pendingCount,
210
243
  settingsActive,
211
244
  workspaceSlug,
212
- contentRoute,
213
245
  }: {
214
246
  pendingCount: number;
215
247
  settingsActive: boolean;
216
248
  workspaceSlug: string;
217
- contentRoute: ContentRoute;
218
249
  }) {
219
250
  const navigate = useNavigate();
220
251
  const location = useLocation();
@@ -222,7 +253,7 @@ function ActivityRail({
222
253
  return (
223
254
  <nav
224
255
  aria-label="Studio"
225
- {...stylex.props(styles.rail, isWorkspaceMediaPath(location.pathname) && styles.railEdge)}
256
+ {...stylex.props(styles.rail)}
226
257
  >
227
258
  <div {...stylex.props(styles.railTop)}>
228
259
  <Tooltip label="Content" side="right">
@@ -231,13 +262,8 @@ function ActivityRail({
231
262
  aria-label="Content"
232
263
  aria-current={isWorkspaceContentPath(location.pathname) ? "page" : undefined}
233
264
  onClick={() => {
234
- if (isWorkspaceMediaPath(location.pathname)) {
235
- navigate(workspaceHomePath(workspaceSlug));
236
- return;
237
- }
238
- if (contentRoute.docSlug && contentRoute.type) {
239
- navigate(contentTypePath(workspaceSlug, contentRoute.type));
240
- }
265
+ const href = contentRailHref(location.pathname, workspaceSlug);
266
+ if (href) navigate(href);
241
267
  }}
242
268
  {...stylex.props(
243
269
  styles.railBtn,
@@ -297,6 +323,7 @@ function ActivityRail({
297
323
  <ExplorerRailSettingsIcon />
298
324
  </Link>
299
325
  </Tooltip>
326
+ <UserMenu compact />
300
327
  </div>
301
328
  </nav>
302
329
  );
@@ -637,7 +664,7 @@ function FolderTreeRow({
637
664
  <ExplorerTwistie open={open} />
638
665
  </span>
639
666
  <span {...stylex.props(styles.treeIcon)}>
640
- <ExplorerFolderIcon open={open} />
667
+ <ExplorerFolderIcon />
641
668
  </span>
642
669
  <span {...stylex.props(styles.treeLabel)}>{node.name}</span>
643
670
  </button>
@@ -833,27 +860,32 @@ function sectionTitle(section: ContentTypeSection) {
833
860
  }
834
861
 
835
862
  const styles = stylex.create({
836
- root: {
863
+ railColumn: {
837
864
  display: "flex",
838
- minHeight: 0,
839
- flex: 1,
840
- flexDirection: "row",
865
+ height: "100%",
866
+ width: "48px",
867
+ minWidth: "48px",
868
+ flexShrink: 0,
869
+ flexDirection: "column",
870
+ borderRightWidth: 1,
871
+ borderRightStyle: "solid",
872
+ borderRightColor: colors.border,
873
+ backgroundColor: colors.bgSurface,
841
874
  },
842
875
  rail: {
843
876
  display: "flex",
844
877
  width: "48px",
878
+ minHeight: 0,
879
+ flex: 1,
845
880
  flexShrink: 0,
846
881
  flexDirection: "column",
847
882
  justifyContent: "space-between",
848
883
  alignItems: "center",
849
- borderRightWidth: 1,
850
- borderRightStyle: "solid",
851
- borderRightColor: colors.border,
852
884
  backgroundColor: colors.bgSidebar,
853
885
  paddingBlock: space.md,
854
886
  },
855
- railEdge: {
856
- borderRightWidth: 0,
887
+ railAccountOnly: {
888
+ justifyContent: "flex-end",
857
889
  },
858
890
  railTop: {
859
891
  display: "flex",
@@ -865,6 +897,7 @@ const styles = stylex.create({
865
897
  display: "flex",
866
898
  flexDirection: "column",
867
899
  alignItems: "center",
900
+ gap: 2,
868
901
  },
869
902
  railBtn: {
870
903
  position: "relative",
@@ -921,10 +954,15 @@ const styles = stylex.create({
921
954
  },
922
955
  panel: {
923
956
  display: "flex",
924
- minWidth: 0,
957
+ width: "calc(min(360px, 38vw) - 48px)",
958
+ minWidth: "252px",
925
959
  minHeight: 0,
926
- flex: 1,
960
+ flexGrow: 0,
961
+ flexShrink: 0,
927
962
  flexDirection: "column",
963
+ borderRightWidth: 1,
964
+ borderRightStyle: "solid",
965
+ borderRightColor: colors.border,
928
966
  backgroundColor: colors.bgSurface,
929
967
  },
930
968
  panelHeader: {
@@ -110,10 +110,12 @@ const styles = stylex.create({
110
110
  },
111
111
  },
112
112
  triggerCompact: {
113
+ width: "36px",
114
+ height: "36px",
115
+ minHeight: "36px",
113
116
  justifyContent: "center",
114
117
  paddingInline: 0,
115
118
  paddingBlock: 0,
116
- minHeight: "32px",
117
119
  },
118
120
  initial: {
119
121
  fontSize: type.sm,
@@ -9,7 +9,6 @@ import {
9
9
  IconFileText,
10
10
  IconFiles,
11
11
  IconFolder1,
12
- IconFolderOpen,
13
12
  IconGit,
14
13
  IconHistory,
15
14
  IconImages1,
@@ -42,9 +41,8 @@ export function ExplorerTwistie({ open }: { open: boolean }) {
42
41
  return <Central icon={Icon} size={TREE} />;
43
42
  }
44
43
 
45
- export function ExplorerFolderIcon({ open }: { open: boolean }) {
46
- const Icon = open ? IconFolderOpen : IconFolder1;
47
- return <Central icon={Icon} size={TREE} />;
44
+ export function ExplorerFolderIcon() {
45
+ return <Central icon={IconFolder1} size={TREE} />;
48
46
  }
49
47
 
50
48
  export function ExplorerFileIcon({ ext }: { ext?: string }) {
@@ -6,7 +6,7 @@ import {
6
6
  IconSquareBehindSquare1,
7
7
  IconTrashCanSimple,
8
8
  } from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
9
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
10
10
  import { useOutletContext, useSearchParams } from "react-router";
11
11
  import * as stylex from "@stylexjs/stylex";
12
12
  import { deleteMedia, fetchMedia, uploadMedia } from "../api";
@@ -22,7 +22,6 @@ import {
22
22
  emptyMediaGrid,
23
23
  failUpload,
24
24
  finishUpload,
25
- gridLength,
26
25
  lightboxImageId,
27
26
  mediaIdAfterRemoval,
28
27
  nextMediaIndex,
@@ -59,7 +58,8 @@ export function MediaGalleryPage({
59
58
  const [settled, setSettled] = useState(false);
60
59
  const dragDepth = useRef(0);
61
60
  const scroller = useRef<HTMLDivElement>(null);
62
- const [scroll, setScroll] = useState({ top: 0, height: 600, width: 800 });
61
+ const grid = useRef<HTMLDivElement>(null);
62
+ const [columns, setColumns] = useState(1);
63
63
  const loadingPage = useRef(false);
64
64
 
65
65
  const load = useCallback(
@@ -84,11 +84,13 @@ export function MediaGalleryPage({
84
84
  void load();
85
85
  }, [load]);
86
86
 
87
- useEffect(() => {
87
+ const maybeLoadMore = useCallback(() => {
88
88
  const node = scroller.current;
89
- if (!node) return;
90
- setScroll({ top: node.scrollTop, height: node.clientHeight || 600, width: node.clientWidth || 800 });
91
- }, [state.pending.length, state.loaded.length]);
89
+ if (!node || !state.nextCursor) return;
90
+ if (node.scrollTop + node.clientHeight > node.scrollHeight - CELL * 2) {
91
+ void load(state.nextCursor);
92
+ }
93
+ }, [load, state.nextCursor]);
92
94
 
93
95
  const uploadFiles = useCallback(
94
96
  (files: File[]) => {
@@ -250,14 +252,8 @@ export function MediaGalleryPage({
250
252
  void load(state.nextCursor);
251
253
  }, [sharedId, browsable, state.nextCursor, load]);
252
254
 
253
- const columns = Math.max(2, Math.floor((scroll.width - 24) / (CELL + 12)));
254
- const count = gridLength(state);
255
- const rows = Math.ceil(count / columns);
256
- const startRow = Math.max(0, Math.floor(scroll.top / (CELL + 12)) - 1);
257
- const endRow = Math.min(rows, Math.ceil((scroll.top + scroll.height) / (CELL + 12)) + 2);
258
-
259
255
  const cells = useMemo(() => {
260
- const out: Array<{ kind: "pending"; item: PendingUpload } | { kind: "media"; item: CatalogMedia } | { kind: "empty" }> = [];
256
+ const out: Array<{ kind: "pending"; item: PendingUpload } | { kind: "media"; item: CatalogMedia }> = [];
261
257
  for (const item of state.pending) {
262
258
  if (item.media) out.push({ kind: "media", item: item.media });
263
259
  else out.push({ kind: "pending", item });
@@ -266,26 +262,24 @@ export function MediaGalleryPage({
266
262
  if (state.pending.some((slot) => slot.media?.id === item.id)) continue;
267
263
  out.push({ kind: "media", item });
268
264
  }
269
- while (out.length < count) out.push({ kind: "empty" });
270
265
  return out;
271
- }, [state.pending, state.loaded, count]);
266
+ }, [state.pending, state.loaded]);
272
267
 
273
- const visible = [];
274
- for (let row = startRow; row < endRow; row++) {
275
- for (let column = 0; column < columns; column++) {
276
- const index = row * columns + column;
277
- if (index >= cells.length) break;
278
- visible.push({ index, row, column, cell: cells[index]! });
279
- }
280
- }
281
-
282
- const maybeLoadMore = () => {
283
- const node = scroller.current;
284
- if (!node || !state.nextCursor) return;
285
- if (node.scrollTop + node.clientHeight > node.scrollHeight - CELL * 2) {
286
- void load(state.nextCursor);
287
- }
288
- };
268
+ useLayoutEffect(() => {
269
+ const scrollNode = scroller.current;
270
+ const gridNode = grid.current;
271
+ if (!scrollNode) return;
272
+ const measure = () => {
273
+ const next = gridNode ? columnsInFirstRow(gridNode) : 1;
274
+ setColumns((current) => (current === next ? current : next));
275
+ maybeLoadMore();
276
+ };
277
+ measure();
278
+ const observer = new ResizeObserver(measure);
279
+ observer.observe(scrollNode);
280
+ if (gridNode) observer.observe(gridNode);
281
+ return () => observer.disconnect();
282
+ }, [maybeLoadMore, cells.length]);
289
283
 
290
284
  useEffect(() => {
291
285
  if (pick || sharedId) return;
@@ -343,19 +337,16 @@ export function MediaGalleryPage({
343
337
  state.pending.length,
344
338
  ]);
345
339
 
346
- useEffect(() => {
340
+ useLayoutEffect(() => {
347
341
  if (!selectedId || sharedId) return;
348
- const index = browsable.findIndex((item) => item.id === selectedId);
349
- const node = scroller.current;
350
- if (index < 0 || !node) return;
351
- const place = mediaPlaces[index];
352
- if (place === undefined) return;
353
- const row = Math.floor(place / columns);
354
- const top = row * (CELL + 12);
355
- const bottom = top + CELL;
356
- if (top < node.scrollTop) node.scrollTop = top;
357
- else if (bottom > node.scrollTop + node.clientHeight) node.scrollTop = bottom - node.clientHeight;
358
- }, [browsable, columns, mediaPlaces, selectedId, sharedId, state.pending.length]);
342
+ const frame = scroller.current;
343
+ const node = grid.current?.querySelector("[aria-current='true']");
344
+ if (!frame || !(node instanceof HTMLElement)) return;
345
+ const item = node.getBoundingClientRect();
346
+ const bounds = frame.getBoundingClientRect();
347
+ if (item.top < bounds.top) frame.scrollTop -= bounds.top - item.top;
348
+ else if (item.bottom > bounds.bottom) frame.scrollTop += item.bottom - bounds.bottom;
349
+ }, [columns, selectedId, sharedId]);
359
350
 
360
351
  const open = lightbox >= 0 ? browsable[lightbox] : undefined;
361
352
  const showEmpty =
@@ -376,9 +367,7 @@ export function MediaGalleryPage({
376
367
  <div
377
368
  ref={scroller}
378
369
  {...stylex.props(styles.scroller)}
379
- onScroll={(event) => {
380
- const node = event.currentTarget;
381
- setScroll({ top: node.scrollTop, height: node.clientHeight, width: node.clientWidth });
370
+ onScroll={() => {
382
371
  maybeLoadMore();
383
372
  }}
384
373
  >
@@ -410,20 +399,14 @@ export function MediaGalleryPage({
410
399
  </div>
411
400
  </div>
412
401
  ) : (
413
- <div {...stylex.props(styles.canvas)} style={{ height: rows * (CELL + 12) }}>
414
- {visible.map(({ index, row, column, cell }) => (
402
+ <div ref={grid} {...stylex.props(styles.canvas)}>
403
+ {cells.map((cell) => (
415
404
  <div
416
- key={cell.kind === "media" ? cell.item.id : cell.kind === "pending" ? cell.item.key : `empty-${index}`}
405
+ key={cell.kind === "media" ? cell.item.id : cell.item.key}
417
406
  {...stylex.props(styles.cell)}
418
- style={{
419
- width: CELL,
420
- height: CELL,
421
- transform: `translate(${column * (CELL + 12)}px, ${row * (CELL + 12)}px)`,
422
- }}
423
407
  >
424
- {cell.kind === "empty" ? <div {...stylex.props(styles.empty)} /> : null}
425
408
  {cell.kind === "pending" ? (
426
- <figure {...stylex.props(styles.figure)}>
409
+ <figure {...stylex.props(styles.figure, styles.placed)}>
427
410
  <img src={cell.item.previewUrl} alt="" {...stylex.props(styles.image, styles.dimmed)} />
428
411
  {cell.item.status === "error" ? (
429
412
  <figcaption {...stylex.props(styles.caption)}>{cell.item.error}</figcaption>
@@ -519,7 +502,7 @@ function MediaHit({
519
502
  <button
520
503
  type="button"
521
504
  aria-current={selected ? "true" : undefined}
522
- {...stylex.props(styles.hit, selected && styles.selected)}
505
+ {...stylex.props(styles.hit, !menu && styles.placed, selected && styles.selected)}
523
506
  onClick={onOpen}
524
507
  >
525
508
  <img src={item.thumbUrl} alt={item.filename} {...stylex.props(styles.image)} />
@@ -572,6 +555,18 @@ function MediaHit({
572
555
  );
573
556
  }
574
557
 
558
+ function columnsInFirstRow(grid: HTMLElement): number {
559
+ const first = grid.children[0];
560
+ if (!(first instanceof HTMLElement)) return 1;
561
+ const top = first.offsetTop;
562
+ let count = 0;
563
+ for (const kid of grid.children) {
564
+ if (!(kid instanceof HTMLElement) || kid.offsetTop - top > 1) break;
565
+ count += 1;
566
+ }
567
+ return Math.max(1, count);
568
+ }
569
+
575
570
  function gridKeyTarget(target: EventTarget | null, scroller: HTMLElement | null): boolean {
576
571
  if (!(target instanceof HTMLElement)) return false;
577
572
  if (target.closest("input, textarea, select, [contenteditable='true']")) return false;
@@ -645,28 +640,27 @@ const styles = stylex.create({
645
640
  },
646
641
  canvas: {
647
642
  position: "relative",
643
+ display: "grid",
644
+ gridTemplateColumns: "repeat(auto-fill, minmax(168px, 1fr))",
645
+ gap: space.lg,
646
+ alignItems: "start",
648
647
  },
649
648
  cell: {
650
- position: "absolute",
651
- top: 0,
652
- left: 0,
653
- boxSizing: "border-box",
654
- padding: 6,
655
- },
656
- empty: {
649
+ position: "relative",
657
650
  width: "100%",
658
- height: "100%",
659
- borderRadius: radius.md,
660
- backgroundColor: colors.bgPanel,
651
+ aspectRatio: "1",
652
+ minWidth: 0,
653
+ },
654
+ placed: {
655
+ position: "absolute",
656
+ inset: space.sm,
661
657
  },
662
658
  figure: {
663
- position: "relative",
664
659
  margin: 0,
665
- height: "100%",
666
- display: "flex",
667
- flexDirection: "column",
668
660
  },
669
661
  hit: {
662
+ position: "absolute",
663
+ inset: 0,
670
664
  display: "block",
671
665
  width: "100%",
672
666
  height: "100%",
@@ -683,9 +677,9 @@ const styles = stylex.create({
683
677
  outlineOffset: 3,
684
678
  },
685
679
  menuHit: {
680
+ position: "absolute",
681
+ inset: space.sm,
686
682
  display: "block",
687
- width: "100%",
688
- height: "100%",
689
683
  },
690
684
  menu: {
691
685
  minWidth: "11rem",
@@ -697,6 +691,8 @@ const styles = stylex.create({
697
691
  color: colors.danger,
698
692
  },
699
693
  image: {
694
+ position: "absolute",
695
+ inset: 0,
700
696
  width: "100%",
701
697
  height: "100%",
702
698
  objectFit: "cover",