Files
RecipesCam/docker/frontend/src/ui/ToolRail.tsx
T
3dtours 35d8d57984 HSL: click a colour chip to open its panel, the way PICK does
The colour band chips only aimed the mixer before; now they open the same
panel the eyedropper opens, seeded with the colour the chip is named after
(hexToRgb turns that readout back into the RGB bytes the picker stores).
A pick already on the photo keeps its spot — only a mixer with nowhere to
hang takes the middle of the frame.

The HSL column itself is untouched: 8 chips, the rule, IMAGE,
HUE/SAT/LUM, the readout and RESET all stay where they were.
2026-09-30 07:34:07 +07:00

90 lines
4.1 KiB
TypeScript

import type { MsgKey } from '../i18n/vi';
import { useI18n } from '../i18n/I18nProvider';
// The app's nine tabs. LIGHT is the develop column and holds every knob of the
// picture — white balance, the tonal range, presence and the effects — so WB and
// FX, which used to be tabs of their own, are panels of it now. TOOLS is the one
// tab of features rather than records: the chips that write a value or take the
// pointer (AUTO, FIX, GRADIENT MASK, MONOCHROME), which used to share LIGHT's
// row with the two chips that are not knobs. The rail is a single column on
// desktop and becomes the bottom bar under 860px (see styles/app.css).
export type TabId = 'presets' | 'favorited' | 'photos' | 'light' | 'tools' | 'hsl' | 'frame' | 'save' | 'create';
// The build this bundle came from, put in by vite at build time and named by
// whoever built the image (see vite.config.ts). Empty under a dev server.
const VERSION = import.meta.env.VITE_VERSION ?? '';
export const TABS: { id: TabId; label: MsgKey; glyph: string }[] = [
{ id: 'presets', label: 'tab.presets', glyph: '◉' },
{ id: 'favorited', label: 'tab.favorited', glyph: '★' },
{ id: 'photos', label: 'tab.photos', glyph: '▤' },
{ id: 'light', label: 'tab.light', glyph: '☀' },
// The hammer and pick: the features that work ON the photo — AUTO's reading,
// FIX's brushes, GRADIENT MASK's shapes, the MONOCHROME switch.
{ id: 'tools', label: 'tab.tools', glyph: '⚒' },
// The HSL mixer sits with the colour tabs: it edits one hue band at a time.
{ id: 'hsl', label: 'tab.hsl', glyph: '◍' },
{ id: 'frame', label: 'tab.frame', glyph: '▣' },
// Keeping the look on the stage and building one from scratch are two acts,
// so they get two tabs: SAVE RECENT files what is open, CREATE RECIPES starts
// a recipe from its parts.
{ id: 'save', label: 'tab.save', glyph: '⤓' },
{ id: 'create', label: 'tab.create', glyph: '✎' },
];
export function ToolRail({
tab,
onTab,
onDropRecipe,
signedIn = false,
}: {
tab: TabId;
onTab: (tab: TabId) => void;
// A recipe chip dragged onto FAVORITED is filed there (see App's favorite()).
onDropRecipe?: (id: string) => void;
// MY PHOTOS is the account's own folder, so the tab is only offered once
// there is an account to hold one.
signedIn?: boolean;
}) {
const { t } = useI18n();
const favorited = (id: TabId) => id === 'favorited';
const tabs = signedIn ? TABS : TABS.filter((entry) => entry.id !== 'photos');
return (
<nav className="rail" aria-label="tool tabs">
{tabs.map((entry) => (
<button
key={entry.id}
type="button"
aria-pressed={tab === entry.id}
onClick={() => onTab(entry.id)}
// The star is a drop target: recipes are dragged onto it from the
// RECIPES strip rather than retyped.
onDragOver={(e) => {
if (!favorited(entry.id) || !onDropRecipe) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
}}
onDrop={(e) => {
if (!favorited(entry.id) || !onDropRecipe) return;
e.preventDefault();
const id = e.dataTransfer.getData('text/plain');
if (id) onDropRecipe(id);
}}
>
<span className="glyph" aria-hidden="true">{entry.glyph}</span>
{/* A box, not a bare text node: on a phone the glyph is display:none
and the name would be the button's only child — an anonymous flex
item, which is the one thing iOS stops painting until a touch. */}
<span className="rail-label">{t(entry.label)}</span>
</button>
))}
{/* The build this page came off, at the foot of the rail: an image on the
NAS carries no other mark, so the version is how an operator tells the
one that is up from the one they just built. Stamped at build time
(vite.config), empty in a dev server, so the line only draws when
there is something to say. */}
{VERSION ? <span className="rail-version">{VERSION}</span> : null}
</nav>
);
}