diff --git a/web/EditorPanel.tsx b/web/EditorPanel.tsx
index 2186794..cb585e8 100644
--- a/web/EditorPanel.tsx
+++ b/web/EditorPanel.tsx
@@ -59,10 +59,21 @@ export const EditorPanel = () => {
)}
- {/* Outside the scroll area, so a long editor cannot carry it off the screen. */}
-
-
-
+ {/*
+ Only where there is a page to look at.
+
+ The toggle sets the width the PREVIEW renders at, and Galleries, Media and Administration have
+ no preview — the control would be three buttons that do nothing visible. Named here rather
+ than read from `usePreviewable`, which is a context inside the detail panel and this is its
+ sibling.
+
+ Outside the scroll area, so a long editor cannot carry it off the screen.
+ */}
+ {(section === 'layout' || section === 'pages') && (
+
+
+
+ )}
{editor ? (
/*
diff --git a/web/sections/GalleriesPanel.tsx b/web/sections/GalleriesPanel.tsx
index 8e47deb..2d089f4 100644
--- a/web/sections/GalleriesPanel.tsx
+++ b/web/sections/GalleriesPanel.tsx
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router';
-import { ArrowDown, ArrowUp, FolderOpen, Images, Loader2, Plus, Trash2 } from 'lucide-react';
+import { Images, Loader2, Plus, Trash2 } from 'lucide-react';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import { getHeaders } from 'hooks/useClient';
import { readableError } from '../errors';
-import { MediaPicker } from '../MediaPicker';
+import { useEditorSlot } from '../useEditorSlot';
+import { GalleryItems } from './GalleryItems';
/**
* Galleries — named, ordered sets of media, reusable anywhere on the site.
@@ -28,19 +28,6 @@ import { MediaPicker } from '../MediaPicker';
type Item = { id: number; url: string; alt: string | null; sort_order: number };
type Gallery = { id: number; name: string; slug: string; items: Item[] };
-/**
- * A thumbnail URL, authenticated by `?token=`.
- *
- * An `` cannot send an Authorization header, which is why `user-middleware` and the permission
- * gate both accept the token in the query string — the same thing the file browser's own thumbnails do.
- * Built without it first, and every picture 401'd while the layout looked perfectly correct.
- */
-const thumbUrl = (path: string): string => {
- const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
- const encoded = path.split('/').map(encodeURIComponent).join('/');
- return `/api/fofos-admin/derived/${encoded}/320.webp?token=${encodeURIComponent(token)}`;
-};
-
export const GalleriesPanel = () => {
const client = useClient();
const clientRef = useRef(client);
@@ -49,7 +36,6 @@ export const GalleriesPanel = () => {
const [galleries, setGalleries] = useState(null);
const [openId, setOpenId] = useState(null);
const [name, setName] = useState('');
- const [picking, setPicking] = useState(null);
/** Paths handed over by the context menu, waiting for a name. */
const [pending, setPending] = useState([]);
const [busy, setBusy] = useState(null);
@@ -70,6 +56,28 @@ export const GalleriesPanel = () => {
void load();
}, [load]);
+ /*
+ The selected gallery's contents, handed to the editor column.
+
+ Announced rather than rendered, because that panel is a SIBLING — see useEditorSlot. `onChanged`
+ reloads the list here, so the item count beside the name is never one behind what is in it.
+
+ The dependency list is everything the editor DISPLAYS: an element captures the props it was built
+ with, so one left out reads as a control that has stopped responding rather than as an error.
+ */
+ const [, setEditor] = useEditorSlot();
+ const setEditorRef = useRef(setEditor);
+ setEditorRef.current = setEditor;
+
+ useEffect(() => {
+ setEditorRef.current(
+ openId === null ? null : void load()} />,
+ );
+ }, [openId, load]);
+
+ // Cleared on unmount, or the panel goes on offering a gallery over somebody else's screen.
+ useEffect(() => () => setEditorRef.current(null), []);
+
/*
Arriving from "Create Gallery" in the file browser.
@@ -120,23 +128,6 @@ export const GalleriesPanel = () => {
setPending([]);
});
- const addTo = (galleryId: number, url: string) =>
- send(`add:${galleryId}`, () => clientRef.current.post(`/fofos-admin/galleries/${galleryId}/items`, { urls: [url] }));
-
- const removeItem = (galleryId: number, itemId: number) =>
- send(`del:${itemId}`, () => clientRef.current.delete(`/fofos-admin/galleries/${galleryId}/items/${itemId}`));
-
- /** Every id, in order — which is what makes two items claiming a position impossible. */
- const move = (gallery: Gallery, index: number, by: -1 | 1) => {
- const next = [...gallery.items];
- const target = index + by;
- if (target < 0 || target >= next.length) return;
- [next[index], next[target]] = [next[target]!, next[index]!];
- return send('order', () =>
- clientRef.current.put(`/fofos-admin/galleries/${gallery.id}/items/order`, { ids: next.map((item) => item.id) }),
- );
- };
-
if (!galleries) return
{error ?? 'Loading…'}
;
return (
@@ -177,125 +168,56 @@ export const GalleriesPanel = () => {
+ {/*
+ The list, and only the list.
+
+ The contents of a gallery — its pictures, their order, their captions — are announced to the
+ editor column and drawn by `GalleryItems`, which is the same component a gallery BLOCK on a
+ page uses. Two copies of that list existed for a day and this is the one that went.
+
+ So this panel is what a master list should be: which galleries there are, making one, deleting
+ one. What is inside the selected one is a different question, answered beside it.
+ */}
{galleries.map((gallery) => {
const open = openId === gallery.id;
return (
-
+
- {open && (
- <>
-
- {gallery.items.map((item, index) => (
-
-
- {index + 1}
-
-
- {/*
- From the DERIVED ladder, which is the whole point of the pipeline: most of these
- are HEIC and a browser cannot show the original at all. 320 is the smallest rung
- and still sharp at this size on a retina screen.
- */}
- {
- event.currentTarget.style.visibility = 'hidden';
- }}
- />
-
-
- {item.url}
- {/*
- Saved on blur rather than on every keystroke: a caption is a sentence somebody
- is in the middle of, and a request per character would be both wasteful and a
- race with itself.
- */}
- {
- if (event.target.value.trim() !== (item.alt ?? '')) {
- void send(`alt:${item.id}`, () =>
- clientRef.current.patch(`/fofos-admin/galleries/${gallery.id}/items/${item.id}`, {
- alt: event.target.value.trim() || null,
- }),
- );
- }
- }}
- placeholder="Caption — describe it, for screen readers"
- className="h-7 text-xs"
- />
-
-
-
-
-
-
-