diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 51fc7c0..8d199c3 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { TABS, ToolRail, type TabId } from './ui/ToolRail'; -import { ChipRow, MiniSlider, SliderRow, type ChipDef, type SliderDef } from './ui/ChipColumn'; +import { ChipRow, ColGrip, MiniSlider, SliderRow, type ChipDef, type SliderDef } from './ui/ChipColumn'; import { ImageStage } from './ui/ImageStage'; import { StraightenRuler } from './ui/StraightenRuler'; import { ToneCurvePanel } from './ui/ToneCurvePanel'; @@ -3291,6 +3291,11 @@ export function Workspace() { // eslint-disable-next-line react-hooks/exhaustive-deps })(); + // The two tabs whose own chips are names to read down rather than buttons to + // press out of a row: their column drops the chip's chrome (ChipRow's `list`) + // and takes a grip. + const listTab = tab === 'photos' || tab === 'favorited'; + // FIX's two brushes: a chip arms one, the wheel sizes it — the chip's own // readout is that size, since the brush has no size buttons to point at — and // CLEAR hands the whole photo back. HEAL repairs a speck of dust, MOSAIC hides @@ -3829,10 +3834,13 @@ export function Workspace() { /> ) : ( - + )} )} + {/* MY PHOTOS and FAVORITED are lists of names rather than buttons, so + their column is the visitor's to widen (see ColGrip). */} + {listTab ? : null} {tab === 'favorited' && saved.length === 0 ?

{t('sec.savedEmpty')}

: null} {/* The mixer's readout: the colour the eyedropper last read, as the RGB bytes and the HSL the band is indexed by. */} @@ -3952,7 +3960,10 @@ export function Workspace() { {/* column 3 — the open chip's sub-chips, or PRESETS' RECIPES list */} {optGroup ? (
- + + {/* PHOTO STYLE is one name per look, so its strip is the visitor's + to widen — the sims' names are longer than the column. */} + {optGroup === 'style' ? : null} {/* A watermark's colour: the presets above are a shortcut, the well under them picks any colour at all. */} {optGroup === 'wmColor' || optGroup === 'gpsColor' ? ( @@ -3995,7 +4006,10 @@ export function Workspace() { {tab === 'presets' && showRecipes ? (
{recipeStrip.length === 0 ?

{t('sec.savedEmpty')}

: null} - + + {/* RECIPES reads the same way MY PHOTOS does: names, and a width + of the visitor's own. */} +
) : null} diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index bdf7420..31dff57 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -105,7 +105,33 @@ padding: 10px; overflow-y: auto; border-right: 1px solid var(--border-soft); + /* The grip on this column's edge is placed against it (see .col-grip). */ + position: relative; } +/* A list read as names instead of as buttons: MY PHOTOS' saved frames, FAVORITED + and RECIPES' looks, PHOTO STYLE's sims. No border, no fill, no pill — the text + alone, with the accent still naming the one in force. */ +.chip-row.list .chip { + border: none; + background: none; + border-radius: 0; + padding: 2px 0; +} +.chip-row.list .chip:hover .chip-label { text-decoration: underline; } +/* The edge of a column, dragged to widen it (see ColGrip): a strip of the cursor + over the border that is already there. `touch-action: none` is what lets a + finger drag it rather than scroll the column. */ +.col-grip { + position: absolute; + top: 0; + right: -4px; + bottom: 0; + width: 9px; + cursor: col-resize; + touch-action: none; + z-index: 3; +} +.col-grip:hover, .col-grip:active { background: var(--accent-soft); } .col:last-child { border-right: none; } .col-slider { width: var(--slider-w); } /* CREATE RECIPES draws a two-up form, so its column is the wide one. */ diff --git a/docker/frontend/src/ui/ChipColumn.tsx b/docker/frontend/src/ui/ChipColumn.tsx index ddebe51..a4525f9 100644 --- a/docker/frontend/src/ui/ChipColumn.tsx +++ b/docker/frontend/src/ui/ChipColumn.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from 'react'; +import { keep, recall } from '../remember'; // One strip at a time — the same shape as the phone's AdjustmentPanel: a ruler // row for the open slider, an options strip for the open group, then the tab's @@ -62,10 +63,13 @@ export interface SliderDef { // `grid` lays the row out as the table a set of same-kind values is read as // (WB's presets, LIGHT's two Color Chromes) instead of the wrapping strip the -// option rows use. -export function ChipRow({ chips, grid, grid3 }: { chips: ChipDef[]; grid?: boolean; grid3?: boolean }) { +// option rows use. `list` is the other end of it: the rows that are a LIST OF +// NAMES — MY PHOTOS' frames, FAVORITED and RECIPES' looks, PHOTO STYLE's sims — +// drop the button's chrome and read as plain text, with only the accent marking +// the one in force (see .chip-row.list). +export function ChipRow({ chips, grid, grid3, list }: { chips: ChipDef[]; grid?: boolean; grid3?: boolean; list?: boolean }) { return ( -
+
{chips.map((chip) => chip.divider ? ( @@ -114,6 +118,48 @@ export function ChipRow({ chips, grid, grid3 }: { chips: ChipDef[]; grid?: boole ); } +// The edge of a column you can drag: the width is written on the column itself +// and remembered (remember.ts), so the next visit opens at the size it was left +// at. Drag, not a panel: the grip appears where the column's edge already is. +// ponytail: the DOM takes the width and React never learns it — a column width +// is presentation, not state, and a re-render carrying it would fight the drag. +export function ColGrip({ col, min = 110, max = 560 }: { col: string; min?: number; max?: number }) { + const ref = useRef(null); + const key = `studio-col-${col}`; + useEffect(() => { + const box = ref.current?.parentElement; + const kept = recall(key); + if (box && kept) box.style.width = kept; + }, [key]); + return ( +
{ + const box = ref.current?.parentElement; + if (!box) return; + e.preventDefault(); + const from = e.clientX; + const start = box.getBoundingClientRect().width; + const move = (m: PointerEvent) => { + box.style.width = `${Math.round(Math.min(max, Math.max(min, start + m.clientX - from)))}px`; + }; + const up = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + keep(key, box.style.width); + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + }} + /> + ); +} + export function SliderRow({ slider }: { slider: SliderDef }) { // The wheel belongs to the open ruler: one notch is one step, held inside the // parameter's own range. The listener is native because React's is passive —