feat(panel): cascading columns, WB colour swatches, stronger HDF glow

Layout
- the panel is a cascade of columns: the rail's tabs, the tab's chips, the
  open chip's sub-chips, then the ruler. A child column no longer hides the
  column it came from (TEMP -> COLOR TEMP keeps TEMP visible); chips stack one
  per row instead of wrapping
- FRAME's WATERMARK opens its own column, so the frame chips stay put
- CREATE RECIPES gets the wide column its two-up form needs

WB colour swatches
- the ruler draws a colour box under the slider that follows the value:
  COLOR TEMP is the Kelvin colour (Tanner Helland), TINT runs green -10 ->
  neutral 0 -> magenta +10

HDF EFFECT
- knee 0.55..0.85 -> 0.45..0.75, blur 0.004+0.015n -> 0.006+0.024n of the
  width, screen alpha 0.15+0.35n -> 0.28+0.52n: a wide halo on the highlights
  instead of a hairline glow. Web copy of toneShader only — the phone keeps
  its own tuning.

Tabs
- rail order is PRESETS, FAVORITED, WB, LIGHT, FX, FRAME, CREATE RECIPES
This commit is contained in:
2026-09-18 06:42:38 +07:00
parent ffdefd2c9c
commit f13fc37b5a
7 changed files with 198 additions and 77 deletions
+14
View File
@@ -27,6 +27,9 @@ export interface SliderDef {
display: string;
// amber value readout once the knob leaves its default (phone's ruler row)
edited: boolean;
// CSS colour the knob is currently driving (WB's temperature/tint), drawn as a
// swatch under the ruler so the hue moves with the number. Absent = no swatch.
swatch?: string;
onChange: (value: number) => void;
// "<" — closes the ruler and reopens the strip it was launched from.
onBack: () => void;
@@ -90,6 +93,17 @@ export function SliderRow({ slider }: { slider: SliderDef }) {
value={slider.value}
onChange={(e) => slider.onChange(Number(e.target.value))}
/>
{slider.swatch ? (
<div
className="swatch"
data-key={`swatch-${slider.key}`}
data-color={slider.swatch}
style={{ background: slider.swatch }}
title={slider.swatch}
>
<span>{slider.swatch}</span>
</div>
) : null}
</div>
</div>
);
+6 -4
View File
@@ -1,15 +1,17 @@
import type { MsgKey } from '../i18n/vi';
import { useI18n } from '../i18n/I18nProvider';
// The app's six tabs, in the same order as the phone rail. The rail is vertical
// on desktop and becomes the bottom bar under 860px (see styles/app.css).
export type TabId = 'presets' | 'favorited' | 'light' | 'wb' | 'fx' | 'frame' | 'create';
// The app's seven tabs. WB leads LIGHT — white balance is the first thing the
// eye corrects, and the web rail draws them in the order the panel opens. The
// rail is a single column on desktop and becomes the bottom bar under 860px
// (see styles/app.css).
export type TabId = 'presets' | 'favorited' | 'wb' | 'light' | 'fx' | 'frame' | 'create';
export const TABS: { id: TabId; label: MsgKey; glyph: string }[] = [
{ id: 'presets', label: 'tab.presets', glyph: '◉' },
{ id: 'favorited', label: 'tab.favorited', glyph: '★' },
{ id: 'light', label: 'tab.light', glyph: '☀' },
{ id: 'wb', label: 'tab.wb', glyph: '◐' },
{ id: 'light', label: 'tab.light', glyph: '☀' },
{ id: 'fx', label: 'tab.fx', glyph: '✦' },
{ id: 'frame', label: 'tab.frame', glyph: '▣' },
{ id: 'create', label: 'tab.create', glyph: '✎' },