feat(studio): MY PHOTOS, FAVORITED, PHOTO STYLE and RECIPES read as a list of names, and their column is the visitor's

This commit is contained in:
locpham
2026-10-10 21:47:34 +07:00
parent 3e7a826f59
commit 22753ef50b
3 changed files with 93 additions and 7 deletions
+18 -4
View File
@@ -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() {
/>
</>
) : (
<ChipRow chips={chips} />
<ChipRow chips={chips} list={listTab} />
)}
</>
)}
{/* MY PHOTOS and FAVORITED are lists of names rather than buttons, so
their column is the visitor's to widen (see ColGrip). */}
{listTab ? <ColGrip col="main" /> : null}
{tab === 'favorited' && saved.length === 0 ? <p className="hint">{t('sec.savedEmpty')}</p> : 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 ? (
<div className="col col-sub strip" data-col="options">
<ChipRow chips={stripChips(optGroup)} />
<ChipRow chips={stripChips(optGroup)} list={optGroup === 'style'} />
{/* 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' ? <ColGrip col="options" /> : 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 ? (
<div className="col col-sub strip" data-col="recipes">
{recipeStrip.length === 0 ? <p className="hint">{t('sec.savedEmpty')}</p> : null}
<ChipRow chips={recipeStrip} />
<ChipRow chips={recipeStrip} list />
{/* RECIPES reads the same way MY PHOTOS does: names, and a width
of the visitor's own. */}
<ColGrip col="recipes" />
</div>
) : null}
+26
View File
@@ -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. */
+49 -3
View File
@@ -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 (
<div className={`chip-row${grid3 ? ' grid-3' : grid ? ' grid' : ''}`}>
<div className={`chip-row${list ? ' list' : ''}${grid3 ? ' grid-3' : grid ? ' grid' : ''}`}>
{chips.map((chip) =>
chip.divider ? (
<span key={chip.key} className="chip-rule" role="separator" aria-orientation="horizontal" />
@@ -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<HTMLDivElement>(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 (
<div
ref={ref}
className="col-grip"
data-key={`col-grip-${col}`}
role="separator"
aria-orientation="vertical"
aria-label={`resize ${col}`}
onPointerDown={(e) => {
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 —