@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.
- package/dist/chunks/{EmbedPublishPage-88VsmOhi.js → EmbedPublishPage-CtIlaeGO.js} +1 -1
- package/dist/chunks/{UiKitPage-X4hkFL2d.js → UiKitPage-DcnSQ-Hf.js} +3 -3
- package/dist/chunks/{WorkspaceRoutes-DXzdWqN3.js → WorkspaceRoutes-DVpUOaMV.js} +13416 -13389
- package/dist/chunks/{dashboard-DCHRI0WD.js → dashboard-bNp6AbKE.js} +3 -3
- package/dist/chunks/{index-BS0-Bvbk.js → index-DoDE-hwB.js} +2 -2
- package/dist/chunks/{status-bar-action-wj1KZ51m.js → status-bar-action-C0fsAWvD.js} +3205 -3199
- package/dist/dashboard.js +2 -2
- package/dist/index.js +4 -4
- package/dist/site-widget.js +2 -2
- package/dist/studio.css +36 -32
- package/package.json +2 -2
- package/src/chrome/AppShell.tsx +47 -10
- package/src/chrome/AppSidebar.tsx +9 -34
- package/src/chrome/ContentExplorerSidebar.tsx +129 -91
- package/src/chrome/UserMenu.tsx +3 -1
- package/src/chrome/explorer-icons.tsx +2 -4
- package/src/pages/MediaGalleryPage.tsx +70 -74
- package/src/pages/MediaLightbox.tsx +103 -96
- package/src/pages/SelectRepositoriesLink.tsx +28 -4
- package/src/pages/SettingsGeneralPage.tsx +25 -28
- package/src/pages/SettingsGithubPage.tsx +413 -124
- package/src/pages/media-grid.ts +1 -1
- package/src/workspace-path.ts +14 -1
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
<
|
|
230
|
+
<RailColumn>
|
|
136
231
|
<ActivityRail
|
|
137
232
|
pendingCount={pendingCount}
|
|
138
233
|
settingsActive={settingsActive}
|
|
139
234
|
workspaceSlug={workspace.workspaceSlug}
|
|
140
|
-
contentRoute={route}
|
|
141
235
|
/>
|
|
142
|
-
{
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
863
|
+
railColumn: {
|
|
837
864
|
display: "flex",
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
856
|
-
|
|
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
|
-
|
|
957
|
+
width: "calc(min(360px, 38vw) - 48px)",
|
|
958
|
+
minWidth: "252px",
|
|
925
959
|
minHeight: 0,
|
|
926
|
-
|
|
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: {
|
package/src/chrome/UserMenu.tsx
CHANGED
|
@@ -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(
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
87
|
+
const maybeLoadMore = useCallback(() => {
|
|
88
88
|
const node = scroller.current;
|
|
89
|
-
if (!node) return;
|
|
90
|
-
|
|
91
|
-
|
|
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 }
|
|
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
|
|
266
|
+
}, [state.pending, state.loaded]);
|
|
272
267
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
|
|
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
|
-
|
|
340
|
+
useLayoutEffect(() => {
|
|
347
341
|
if (!selectedId || sharedId) return;
|
|
348
|
-
const
|
|
349
|
-
const node =
|
|
350
|
-
if (
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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={(
|
|
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)}
|
|
414
|
-
{
|
|
402
|
+
<div ref={grid} {...stylex.props(styles.canvas)}>
|
|
403
|
+
{cells.map((cell) => (
|
|
415
404
|
<div
|
|
416
|
-
key={cell.kind === "media" ? cell.item.id : cell.
|
|
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: "
|
|
651
|
-
top: 0,
|
|
652
|
-
left: 0,
|
|
653
|
-
boxSizing: "border-box",
|
|
654
|
-
padding: 6,
|
|
655
|
-
},
|
|
656
|
-
empty: {
|
|
649
|
+
position: "relative",
|
|
657
650
|
width: "100%",
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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",
|