web: the preset tester picks its recipe from a grouped dropdown
The row of pills grew as the library did, so the landing's live tester now offers the stocks through one native select, grouped into Landscape, Portrait and Streetlife, five stocks each — fifteen in all. Choosing one still lands on the preview immediately: the frame, the HUD and the spec card all follow the selection. Stock names stay proper nouns, the group names are translated with the rest of the page.
This commit is contained in:
@@ -111,8 +111,11 @@ const REEL: Omit<Look, 'rateKey'>[] = [
|
||||
{ tag: '#ETRNA_VINTAGE', title: { en: 'Faded Postcard', vi: 'Bưu thiếp bạc màu' }, meta: 'ISO 200 · GRAIN 25 · WARMTH +10', photo: SAMPLE(1015), filter: 'sepia(.28) saturate(.9) contrast(.94) brightness(1.08)' },
|
||||
];
|
||||
|
||||
type PresetGroup = 'landscape' | 'portrait' | 'streetlife';
|
||||
|
||||
interface Preset {
|
||||
id: string;
|
||||
group: PresetGroup;
|
||||
// Stock and recipe names are proper nouns — never translated (see i18n/vi.ts).
|
||||
label: string;
|
||||
name: string;
|
||||
@@ -122,12 +125,32 @@ interface Preset {
|
||||
metrics: [number, number, number, number];
|
||||
}
|
||||
|
||||
// The tester's dropdown is grouped by genre, five stocks each.
|
||||
const PRESET_GROUPS: { id: PresetGroup; label: Txt }[] = [
|
||||
{ id: 'landscape', label: { en: 'Landscape', vi: 'Phong cảnh' } },
|
||||
{ id: 'portrait', label: { en: 'Portrait', vi: 'Chân dung' } },
|
||||
{ id: 'streetlife', label: { en: 'Streetlife', vi: 'Đường phố' } },
|
||||
];
|
||||
|
||||
const PRESETS: Preset[] = [
|
||||
{ id: 'portra', label: 'Kodak Portra 400', name: 'PORTRA 400', desc: { en: 'Warm daylight stock, forgiving highlights and natural skin tones — the everyday portrait recipe.', vi: 'Phim chân dung ánh sáng ban ngày ấm, vùng sáng mềm và tông da tự nhiên — công thức chụp hằng ngày.' }, filter: 'sepia(.18) saturate(1.15) contrast(1.03) brightness(1.05)', metrics: [72, 34, 48, 22] },
|
||||
{ id: 'chrome', label: 'Fuji Classic Chrome', name: 'CLASSIC CHROME', desc: { en: 'Deep greens, muted reds and a documentary edge straight out of the rangefinder era.', vi: 'Xanh lá sâu, đỏ trầm và chất tài liệu đúng kiểu máy rangefinder.' }, filter: 'saturate(.76) contrast(1.16) hue-rotate(-8deg)', metrics: [38, 22, 68, 14] },
|
||||
{ id: 'sunset', label: 'Warm Sunset Glow', name: 'SUNSET GLOW', desc: { en: 'Amber backlight with a soft halation around every highlight, made for late golden hour.', vi: 'Ngược sáng hổ phách với quầng sáng mềm quanh vùng cháy, dành cho giờ vàng muộn.' }, filter: 'sepia(.34) saturate(1.35) contrast(1.06) brightness(1.06)', metrics: [88, 28, 52, 61] },
|
||||
{ id: 'cyber', label: "Cyber Neon '88", name: 'CYBER NEON 88', desc: { en: 'Magenta and cyan push, crushed blacks and a bloom that reads like wet asphalt at night.', vi: 'Đẩy magenta và cyan, đen bị nén và quầng sáng như nhựa đường ướt ban đêm.' }, filter: 'saturate(1.8) contrast(1.3) hue-rotate(266deg)', metrics: [24, 46, 84, 78] },
|
||||
{ id: 'mono', label: 'Monochromatic Film', name: 'ACROS 100', desc: { en: 'High-contrast black and white with a fine grain structure and clean mid-tone separation.', vi: 'Đen trắng tương phản cao, hạt mịn và vùng trung tính tách bạch.' }, filter: 'grayscale(1) contrast(1.4) brightness(1.02)', metrics: [6, 41, 92, 18] },
|
||||
// --- landscape ---------------------------------------------------------
|
||||
{ id: 'velvia', group: 'landscape', label: 'Fuji Velvia 50', name: 'VELVIA 50', desc: { en: 'Saturated slide film for moss, ridge lines and sunset skies — colour that refuses to whisper.', vi: 'Phim slide bão hoà cho rêu, sống núi và trời hoàng hôn — màu không chịu thì thầm.' }, filter: 'saturate(1.5) contrast(1.24) hue-rotate(-5deg)', metrics: [20, 26, 82, 30] },
|
||||
{ id: 'ektar', group: 'landscape', label: 'Kodak Ektar 100', name: 'EKTAR 100', desc: { en: 'Ultra-fine grain and punchy primaries, the wide-open landscape stock.', vi: 'Hạt cực mịn, màu gốc căng — phim phong cảnh khẩu độ mở.' }, filter: 'saturate(1.32) contrast(1.14) sepia(.06)', metrics: [46, 20, 72, 34] },
|
||||
{ id: 'provia', group: 'landscape', label: 'Fuji Provia 100F', name: 'PROVIA 100F', desc: { en: 'Neutral slide film that holds the sky and the shadows without pushing either.', vi: 'Phim slide trung tính giữ được cả trời và vùng tối mà không đẩy bên nào.' }, filter: 'saturate(1.14) contrast(1.1) brightness(1.03)', metrics: [34, 24, 64, 22] },
|
||||
{ id: 'vivid', group: 'landscape', label: 'Classic Vivid', name: 'CLASSIC VIVID', desc: { en: 'The camera’s own vivid profile: deep blues, warm rock, nothing held back.', vi: 'Profile vivid của máy: xanh sâu, đá ấm, không giữ lại gì.' }, filter: 'saturate(1.22) contrast(1.2)', metrics: [30, 28, 76, 26] },
|
||||
{ id: 'sunset', group: 'landscape', label: 'Warm Sunset Glow', name: 'SUNSET GLOW', desc: { en: 'Amber backlight with a soft halation around every highlight, made for late golden hour.', vi: 'Ngược sáng hổ phách với quầng sáng mềm quanh vùng cháy, dành cho giờ vàng muộn.' }, filter: 'sepia(.34) saturate(1.35) contrast(1.06) brightness(1.06)', metrics: [88, 28, 52, 61] },
|
||||
// --- portrait ----------------------------------------------------------
|
||||
{ id: 'portra', group: 'portrait', label: 'Kodak Portra 400', name: 'PORTRA 400', desc: { en: 'Warm daylight stock, forgiving highlights and natural skin tones — the everyday portrait recipe.', vi: 'Phim chân dung ánh sáng ban ngày ấm, vùng sáng mềm và tông da tự nhiên — công thức chụp hằng ngày.' }, filter: 'sepia(.18) saturate(1.15) contrast(1.03) brightness(1.05)', metrics: [72, 34, 48, 22] },
|
||||
{ id: 'astia', group: 'portrait', label: 'Fuji Astia', name: 'ASTIA', desc: { en: 'Soft contrast and gentle skin rendering, the portrait film that flatters without lying.', vi: 'Tương phản mềm, da lên dịu — phim chân dung làm đẹp mà không nói dối.' }, filter: 'sepia(.1) saturate(1.06) contrast(.98) brightness(1.06)', metrics: [62, 30, 42, 26] },
|
||||
{ id: 'eterna', group: 'portrait', label: 'Fuji Eterna', name: 'ETERNA', desc: { en: 'Cinema negative: muted highlights, even skin, the grade of a quiet two-shot.', vi: 'Phim âm bản điện ảnh: vùng sáng trầm, da đều, đúng tông cảnh hai người.' }, filter: 'sepia(.07) saturate(.88) contrast(1.06) brightness(1.02)', metrics: [54, 26, 50, 20] },
|
||||
{ id: 'classicneg', group: 'portrait', label: 'Classic Negative', name: 'CLASSIC NEG', desc: { en: 'Amber shadows and restrained skin — the modern editorial portrait look.', vi: 'Vùng tối hổ phách, da tiết chế — chất chân dung editorial hiện đại.' }, filter: 'sepia(.12) saturate(.94) contrast(1.16)', metrics: [66, 32, 60, 24] },
|
||||
{ id: 'gold', group: 'portrait', label: 'Kodak Gold 200', name: 'GOLD 200', desc: { en: 'Sunlit warmth, honeyed highlights and the grain of a family album.', vi: 'Ấm nắng, vùng sáng mật ong và hạt phim như album gia đình.' }, filter: 'sepia(.22) saturate(1.18) contrast(1.04) brightness(1.05)', metrics: [78, 36, 46, 30] },
|
||||
// --- streetlife --------------------------------------------------------
|
||||
{ id: 'chrome', group: 'streetlife', label: 'Fuji Classic Chrome', name: 'CLASSIC CHROME', desc: { en: 'Deep greens, muted reds and a documentary edge straight out of the rangefinder era.', vi: 'Xanh lá sâu, đỏ trầm và chất tài liệu đúng kiểu máy rangefinder.' }, filter: 'saturate(.76) contrast(1.16) hue-rotate(-8deg)', metrics: [38, 22, 68, 14] },
|
||||
{ id: 'cyber', group: 'streetlife', label: "Cyber Neon '88", name: 'CYBER NEON 88', desc: { en: 'Magenta and cyan push, crushed blacks and a bloom that reads like wet asphalt at night.', vi: 'Đẩy magenta và cyan, đen bị nén và quầng sáng như nhựa đường ướt ban đêm.' }, filter: 'saturate(1.8) contrast(1.3) hue-rotate(266deg)', metrics: [24, 46, 84, 78] },
|
||||
{ id: 'mono', group: 'streetlife', label: 'Monochromatic Film', name: 'ACROS 100', desc: { en: 'High-contrast black and white with a fine grain structure and clean mid-tone separation.', vi: 'Đen trắng tương phản cao, hạt mịn và vùng trung tính tách bạch.' }, filter: 'grayscale(1) contrast(1.4) brightness(1.02)', metrics: [6, 41, 92, 18] },
|
||||
{ id: 'streetclassic', group: 'streetlife', label: 'LC Streetlife Classic', name: 'STREETLIFE CLASSIC', desc: { en: 'Cool shadows and hard mid-tones for concrete, signage and passing crowds.', vi: 'Vùng tối lạnh, trung tính gắt cho bê tông, biển hiệu và dòng người qua.' }, filter: 'saturate(.82) contrast(1.24) hue-rotate(-6deg) brightness(.98)', metrics: [30, 38, 74, 16] },
|
||||
{ id: 'streetvivid', group: 'streetlife', label: 'LC Streetlife Vivid', name: 'STREETLIFE VIVID', desc: { en: 'The same street, turned up: tougher contrast, louder colour, night-market energy.', vi: 'Vẫn đường phố đó, vặn to hơn: tương phản gắt, màu rực, năng lượng chợ đêm.' }, filter: 'saturate(1.4) contrast(1.28) hue-rotate(8deg)', metrics: [26, 40, 80, 34] },
|
||||
];
|
||||
|
||||
const BAR_LABELS: Txt[] = [
|
||||
@@ -678,17 +701,28 @@ export function Landing() {
|
||||
<div className="lp-sec-head">
|
||||
<span className="lp-eyebrow">{c({ en: 'Live preset tester', vi: 'Thử công thức trực tiếp' })}</span>
|
||||
<h2>{c({ en: 'Tap a recipe, watch the negative change', vi: 'Chọn công thức, xem âm bản đổi ngay' })}</h2>
|
||||
<p>{c({ en: 'Five stocks from the library, applied to the same frame with the same colour maths the exporter uses.', vi: 'Năm loại phim trong thư viện, áp lên cùng một khung với đúng phép màu mà bộ xuất ảnh dùng.' })}</p>
|
||||
<p>{c({ en: 'Fifteen stocks across landscape, portrait and streetlife, applied to the same frame with the same colour maths the exporter uses.', vi: 'Mười lăm loại phim cho phong cảnh, chân dung và đường phố, áp lên cùng một khung với đúng phép màu mà bộ xuất ảnh dùng.' })}</p>
|
||||
</div>
|
||||
<div className="lp-tester">
|
||||
<div>
|
||||
<div className="lp-tabs" role="tablist" aria-label={c({ en: 'Color presets', vi: 'Công thức màu' })}>
|
||||
{PRESETS.map((p) => (
|
||||
<button key={p.id} type="button" role="tab" className="lp-tab" aria-pressed={p.id === presetId} onClick={() => setPresetId(p.id)}>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<label className="lp-preset-pick">
|
||||
<span className="lp-eyebrow">{c({ en: 'Genre & recipe', vi: 'Thể loại & công thức' })}</span>
|
||||
<select
|
||||
className="lp-select"
|
||||
data-key="lp-preset"
|
||||
aria-label={c({ en: 'Color presets', vi: 'Công thức màu' })}
|
||||
value={presetId}
|
||||
onChange={(e) => setPresetId(e.target.value)}
|
||||
>
|
||||
{PRESET_GROUPS.map((g) => (
|
||||
<optgroup key={g.id} label={c(g.label)}>
|
||||
{PRESETS.filter((p) => p.group === g.id).map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="lp-view">
|
||||
<Shot
|
||||
src={slotPhoto.testers[(slotPhoto.testerStart + testerIdx) % slotPhoto.testers.length]}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[data-theme="light"] .lp-glow { opacity: 0.5; }
|
||||
[data-theme="light"] .lp-btn,
|
||||
[data-theme="light"] .lp-tool,
|
||||
[data-theme="light"] .lp-tab { background: rgba(20, 22, 28, 0.04); }
|
||||
[data-theme="light"] .lp-select { background: rgba(20, 22, 28, 0.04); }
|
||||
[data-theme="light"] .lp-stat { background: linear-gradient(180deg, rgba(20, 22, 28, 0.04), transparent); }
|
||||
[data-theme="light"] .lp-bar i { background: rgba(20, 22, 28, 0.12); }
|
||||
[data-theme="light"] .lp-frame { background: #ffffff; }
|
||||
@@ -329,18 +329,23 @@ html { scroll-behavior: smooth; scroll-padding-top: 116px; }
|
||||
|
||||
/* --- 4. preset tester --------------------------------------------------- */
|
||||
.lp-tester { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 26px; align-items: start; }
|
||||
.lp-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
||||
.lp-tab {
|
||||
.lp-preset-pick { display: block; margin-bottom: 16px; }
|
||||
.lp-preset-pick .lp-eyebrow { display: block; margin-bottom: 8px; }
|
||||
.lp-select {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border: 1px solid var(--lp-line);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--lp-muted);
|
||||
border-radius: 999px;
|
||||
padding: 7px 14px;
|
||||
color: var(--lp-text);
|
||||
border-radius: 10px;
|
||||
padding: 9px 12px;
|
||||
font-size: 12.5px;
|
||||
font-family: var(--lp-mono);
|
||||
cursor: pointer;
|
||||
}
|
||||
.lp-tab[aria-pressed="true"] { border-color: var(--lp-amber); color: var(--lp-ink-amber); background: var(--accent-soft); }
|
||||
.lp-select:hover, .lp-select:focus-visible { border-color: var(--lp-amber); color: var(--lp-ink-amber); outline: none; }
|
||||
.lp-select optgroup { background: var(--lp-card); color: var(--lp-muted); font-family: var(--lp-mono); font-size: 11px; }
|
||||
.lp-select option { background: var(--lp-card); color: var(--lp-text); font-family: var(--lp-mono); font-size: 12.5px; }
|
||||
.lp-view { position: relative; border-radius: 16px; overflow: hidden; border: 1px solid var(--lp-line); background: #000; }
|
||||
.lp-view img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; transition: filter 380ms ease; }
|
||||
.lp-arrow {
|
||||
|
||||
Reference in New Issue
Block a user