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" - /> - - - - - - -
  • - ))} - {!gallery.items.length &&
  • Nothing in it yet.
  • } -
- -
- - -
- - )} +
); })} - setPicking(null)} - onPick={(path) => { - if (picking !== null) void addTo(picking, path); - }} - title="Add to gallery" - /> ); };