Files
RecipesCam/docker/frontend/src/ui/ToolRail.tsx
T
3dtours cbcfe353ab feat(studio): draw every slider as a line with a hollow node, and move the shapes into TOOLS
The default ring was a solid disc on a coloured bar, which read as a crude
block next to the rest of the rail; every range input is now a hairline
track with an outline node in the accent colour, and the temperature and
tint strips keep their gradients on the track itself.

CROP, ROTATE and WATERMARK reshape the frame rather than grade it, so they
belong with the tools that work on the whole image.
2026-10-08 11:28:14 +07:00

92 lines
4.2 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, and — since they act on the frame
// rather than pick a look for it — CROP, ROTATE and WATERMARK, which used to
// close the frame row. 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>
);
}