Expand style picker: 12 directions with per-page mockups.
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 31s
CI / python-lint (pull_request) Successful in 33s
CI / secret-scan (pull_request) Successful in 38s
CI / viewer-unit (pull_request) Successful in 1m45s
CI / admin-unit (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m45s
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 31s
CI / python-lint (pull_request) Successful in 33s
CI / secret-scan (pull_request) Successful in 38s
CI / viewer-unit (pull_request) Successful in 1m45s
CI / admin-unit (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m45s
Add Matchmaker Arcade, Parade Mosaic, and Orbit Kin; show Home, Photo, Search, Identify, Upload, and Login for each style.
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
'use client';
|
||||
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { PageTypeId, StyleOption, StyleTokens } from './styleOptions';
|
||||
|
||||
function shellStyle(t: StyleTokens): CSSProperties {
|
||||
return {
|
||||
background: t.bg,
|
||||
color: t.text,
|
||||
fontFamily: t.body,
|
||||
borderRadius: t.radius === '999px' ? '16px' : t.radius,
|
||||
border: `1px solid ${t.border}`,
|
||||
};
|
||||
}
|
||||
|
||||
function HeaderBar({ t, title }: { t: StyleTokens; title: string }) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 px-2.5 py-1.5 border-b"
|
||||
style={{ borderColor: t.border, background: t.surface }}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="text-[8px] font-bold tracking-wide truncate"
|
||||
style={{ fontFamily: t.display, color: t.accent }}
|
||||
>
|
||||
JRCC
|
||||
</div>
|
||||
<div className="text-[9px] truncate opacity-90" style={{ color: t.muted }}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<span
|
||||
className="text-[8px] px-1.5 py-0.5 font-semibold"
|
||||
style={{
|
||||
background: t.accent,
|
||||
color: t.accentText,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '4px',
|
||||
}}
|
||||
>
|
||||
menu
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PhotoTile({
|
||||
t,
|
||||
tone,
|
||||
label,
|
||||
tall,
|
||||
}: {
|
||||
t: StyleTokens;
|
||||
tone: 'A' | 'B' | 'C';
|
||||
label?: string;
|
||||
tall?: boolean;
|
||||
}) {
|
||||
const bg = tone === 'A' ? t.photoA : tone === 'B' ? t.photoB : t.photoC;
|
||||
return (
|
||||
<div
|
||||
className={cn('relative overflow-hidden flex items-end', tall ? 'aspect-[3/4]' : 'aspect-square')}
|
||||
style={{
|
||||
background: bg,
|
||||
borderRadius: t.vibe === 'arcade' ? '4px' : t.radius === '999px' ? '12px' : t.radius,
|
||||
boxShadow: t.vibe === 'cinematic' ? `0 0 0 1px ${t.border}` : undefined,
|
||||
}}
|
||||
>
|
||||
{t.vibe === 'festive' && (
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-1"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(90deg, ${t.accent} 0 6px, ${t.photoC} 6px 12px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{label && (
|
||||
<span
|
||||
className="m-1 text-[7px] px-1 py-0.5 font-medium"
|
||||
style={{
|
||||
background: `${t.surface}cc`,
|
||||
color: t.text,
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockHome({ t }: { t: StyleTokens }) {
|
||||
const gap = t.density === 'airy' ? 'gap-2' : t.density === 'dense' ? 'gap-1' : 'gap-1.5';
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<HeaderBar t={t} title="Browse our photo collection" />
|
||||
<div className="px-2.5 pt-2 pb-1 flex items-center justify-between">
|
||||
<div style={{ fontFamily: t.display }} className="text-[11px] font-semibold leading-tight">
|
||||
{t.vibe === 'arcade'
|
||||
? 'High scores tonight'
|
||||
: t.vibe === 'festive'
|
||||
? 'Parade wall · live'
|
||||
: t.vibe === 'cinematic' && t.radius === '999px'
|
||||
? 'Your constellation'
|
||||
: 'This week at JRCC'}
|
||||
</div>
|
||||
<span className="text-[8px]" style={{ color: t.muted }}>
|
||||
248 photos
|
||||
</span>
|
||||
</div>
|
||||
{t.vibe === 'cinematic' && t.radius === '999px' ? (
|
||||
<div className="relative h-[118px] mx-2 mb-2">
|
||||
{[
|
||||
{ x: '18%', y: '20%', s: 28 },
|
||||
{ x: '48%', y: '8%', s: 36 },
|
||||
{ x: '72%', y: '28%', s: 24 },
|
||||
{ x: '38%', y: '55%', s: 30 },
|
||||
{ x: '62%', y: '58%', s: 22 },
|
||||
].map((n, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full border"
|
||||
style={{
|
||||
left: n.x,
|
||||
top: n.y,
|
||||
width: n.s,
|
||||
height: n.s,
|
||||
background: i % 2 ? t.photoB : t.photoC,
|
||||
borderColor: t.accent,
|
||||
boxShadow: `0 0 12px ${t.accent}55`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none opacity-50">
|
||||
<line x1="28%" y1="30%" x2="55%" y2="20%" stroke={t.accent} strokeWidth="0.5" />
|
||||
<line x1="55%" y1="20%" x2="78%" y2="38%" stroke={t.accent} strokeWidth="0.5" />
|
||||
<line x1="48%" y1="22%" x2="48%" y2="62%" stroke={t.accent} strokeWidth="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn('grid grid-cols-3 px-2.5 pb-2.5', gap)}>
|
||||
<PhotoTile t={t} tone="A" label={t.vibe === 'festive' ? 'named' : undefined} />
|
||||
<PhotoTile t={t} tone="B" />
|
||||
<PhotoTile t={t} tone="C" label={t.vibe === 'arcade' ? '+120' : undefined} />
|
||||
<PhotoTile t={t} tone="B" />
|
||||
<PhotoTile t={t} tone="C" />
|
||||
<PhotoTile t={t} tone="A" />
|
||||
</div>
|
||||
)}
|
||||
{t.vibe === 'playful' || t.vibe === 'arcade' ? (
|
||||
<div
|
||||
className="mx-2.5 mb-2.5 px-2 py-1.5 flex items-center justify-between text-[9px] font-semibold"
|
||||
style={{
|
||||
background: t.accent,
|
||||
color: t.accentText,
|
||||
borderRadius: t.radius === '999px' ? '999px' : t.radius,
|
||||
}}
|
||||
>
|
||||
<span>{t.vibe === 'arcade' ? 'START DAILY MATCH' : 'Daily challenge · 3/5'}</span>
|
||||
<span>→</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockPhoto({ t }: { t: StyleTokens }) {
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<HeaderBar t={t} title="Photo · Aug 2024" />
|
||||
<div className="p-2 flex gap-2">
|
||||
<div
|
||||
className="flex-1 aspect-[4/3] relative"
|
||||
style={{
|
||||
background: `linear-gradient(145deg, ${t.photoA}, ${t.photoB})`,
|
||||
borderRadius: t.radius === '999px' ? '12px' : t.radius,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-[28%] top-[22%] w-8 h-8 rounded-full border-2"
|
||||
style={{ borderColor: t.accent, boxShadow: `0 0 0 1px ${t.accentText}` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-[24%] top-[36%] w-7 h-7 rounded-full border-2 opacity-70"
|
||||
style={{ borderColor: t.muted }}
|
||||
/>
|
||||
{t.vibe === 'arcade' && (
|
||||
<span
|
||||
className="absolute bottom-1 left-1 text-[7px] px-1 py-0.5 font-bold"
|
||||
style={{ background: t.accent, color: t.accentText }}
|
||||
>
|
||||
COMBO x3
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[72px] shrink-0 space-y-1.5">
|
||||
<div className="text-[8px] font-semibold" style={{ fontFamily: t.display }}>
|
||||
Faces
|
||||
</div>
|
||||
{['Anna K.', '???', 'Misha'].map((name, i) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="w-full text-left text-[8px] px-1.5 py-1 border"
|
||||
style={{
|
||||
background: i === 1 ? t.accent : t.surface,
|
||||
color: i === 1 ? t.accentText : t.text,
|
||||
borderColor: t.border,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '6px',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
<div className="text-[7px]" style={{ color: t.muted }}>
|
||||
Tap unknown to name
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockSearch({ t }: { t: StyleTokens }) {
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<HeaderBar t={t} title="Search people & tags" />
|
||||
<div className="p-2.5 space-y-2">
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2 py-1.5 text-[9px] border"
|
||||
style={{
|
||||
background: t.surface,
|
||||
borderColor: t.border,
|
||||
borderRadius: t.radius === '999px' ? '999px' : t.radius,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: t.muted }}>⌕</span>
|
||||
<span style={{ color: t.muted }}>Levkin · Purim · 2019</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['People', 'Tags', 'Year'].map((f) => (
|
||||
<span
|
||||
key={f}
|
||||
className="text-[8px] px-1.5 py-0.5"
|
||||
style={{
|
||||
background: t.chip,
|
||||
color: t.text,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '4px',
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<PhotoTile t={t} tone="A" />
|
||||
<PhotoTile t={t} tone="B" />
|
||||
<PhotoTile t={t} tone="C" />
|
||||
<PhotoTile t={t} tone="A" />
|
||||
</div>
|
||||
<div className="text-[8px]" style={{ color: t.muted }}>
|
||||
18 matches · sorted by date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockIdentify({ t }: { t: StyleTokens }) {
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<HeaderBar t={t} title="Identify · help grow the album" />
|
||||
<div className="p-2.5 flex gap-2">
|
||||
<div
|
||||
className="w-[88px] aspect-square shrink-0"
|
||||
style={{
|
||||
background: `radial-gradient(circle at 40% 35%, ${t.photoC}, ${t.photoA})`,
|
||||
borderRadius: t.radius === '999px' ? '999px' : t.radius,
|
||||
boxShadow: t.vibe === 'arcade' ? `0 0 16px ${t.accent}` : undefined,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
<div className="text-[10px] font-semibold" style={{ fontFamily: t.display }}>
|
||||
{t.vibe === 'arcade' ? 'WHO IS THIS?' : 'Who is this person?'}
|
||||
</div>
|
||||
<div
|
||||
className="h-6 border px-1.5 flex items-center text-[8px]"
|
||||
style={{
|
||||
borderColor: t.border,
|
||||
background: t.surface,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '6px',
|
||||
color: t.muted,
|
||||
}}
|
||||
>
|
||||
Type a name…
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<div
|
||||
className="h-5 border px-1 flex items-center text-[7px]"
|
||||
style={{ borderColor: t.border, borderRadius: '4px', color: t.muted }}
|
||||
>
|
||||
email (opt)
|
||||
</div>
|
||||
<div
|
||||
className="h-5 border px-1 flex items-center text-[7px]"
|
||||
style={{ borderColor: t.border, borderRadius: '4px', color: t.muted }}
|
||||
>
|
||||
phone (opt)
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-[9px] font-bold py-1"
|
||||
style={{
|
||||
background: t.accent,
|
||||
color: t.accentText,
|
||||
borderRadius: t.radius === '999px' ? '999px' : t.radius,
|
||||
}}
|
||||
>
|
||||
{t.vibe === 'arcade' ? 'LOCK IN +50' : 'Submit for approval'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockUpload({ t }: { t: StyleTokens }) {
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<HeaderBar t={t} title="Upload photos" />
|
||||
<div className="p-2.5">
|
||||
<div
|
||||
className="border-2 border-dashed px-3 py-5 text-center"
|
||||
style={{
|
||||
borderColor: t.accent,
|
||||
background: `${t.surface}`,
|
||||
borderRadius: t.radius === '999px' ? '16px' : t.radius,
|
||||
}}
|
||||
>
|
||||
<div className="text-[11px] font-semibold" style={{ fontFamily: t.display }}>
|
||||
{t.vibe === 'festive' ? 'Dump the parade here' : 'Drop photos'}
|
||||
</div>
|
||||
<div className="text-[8px] mt-1" style={{ color: t.muted }}>
|
||||
JPG · PNG · HEIC · up to 50
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 text-[8px] font-semibold px-2.5 py-1"
|
||||
style={{
|
||||
background: t.accent,
|
||||
color: t.accentText,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '6px',
|
||||
}}
|
||||
>
|
||||
Choose files
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-1">
|
||||
<PhotoTile t={t} tone="A" />
|
||||
<PhotoTile t={t} tone="B" />
|
||||
<PhotoTile t={t} tone="C" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MockLogin({ t }: { t: StyleTokens }) {
|
||||
return (
|
||||
<div style={shellStyle(t)} className="overflow-hidden text-left shadow-md">
|
||||
<div className="px-3 pt-4 pb-3 text-center">
|
||||
<div
|
||||
className="text-[12px] font-bold"
|
||||
style={{ fontFamily: t.display, color: t.accent }}
|
||||
>
|
||||
JRCC Gallery
|
||||
</div>
|
||||
<div className="text-[8px] mt-0.5" style={{ color: t.muted }}>
|
||||
Sign in to tag faces & upload
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 pb-3 space-y-1.5">
|
||||
{['Email', 'Password'].map((label) => (
|
||||
<div
|
||||
key={label}
|
||||
className="h-7 border px-2 flex items-center text-[8px]"
|
||||
style={{
|
||||
borderColor: t.border,
|
||||
background: t.surface,
|
||||
borderRadius: t.radius === '999px' ? '999px' : '6px',
|
||||
color: t.muted,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-[9px] font-bold py-1.5 mt-1"
|
||||
style={{
|
||||
background: t.accent,
|
||||
color: t.accentText,
|
||||
borderRadius: t.radius === '999px' ? '999px' : t.radius,
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
<div className="text-center text-[7px]" style={{ color: t.muted }}>
|
||||
New here? Create an account
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MOCKS: Record<PageTypeId, (props: { t: StyleTokens }) => ReactNode> = {
|
||||
home: MockHome,
|
||||
photo: MockPhoto,
|
||||
search: MockSearch,
|
||||
identify: MockIdentify,
|
||||
upload: MockUpload,
|
||||
login: MockLogin,
|
||||
};
|
||||
|
||||
export function StylePageGallery({ style }: { style: StyleOption }) {
|
||||
const t = style.tokens;
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(MOCKS).map(([id, Mock]) => {
|
||||
const meta = (
|
||||
[
|
||||
['home', 'Home / browse'],
|
||||
['photo', 'Photo viewer'],
|
||||
['search', 'Search'],
|
||||
['identify', 'Identify face'],
|
||||
['upload', 'Upload'],
|
||||
['login', 'Login'],
|
||||
] as const
|
||||
).find(([k]) => k === id)!;
|
||||
return (
|
||||
<div key={id} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between px-0.5">
|
||||
<span className="text-[11px] font-medium text-foreground">{meta[1]}</span>
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wide">
|
||||
mock
|
||||
</span>
|
||||
</div>
|
||||
<Mock t={t} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,199 +5,26 @@ import Link from 'next/link';
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
import { JrccLogo } from '@/components/JrccLogo';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { StylePageGallery } from './PageMockups';
|
||||
import { STYLES, type StyleId } from './styleOptions';
|
||||
|
||||
const STORAGE_KEY = 'jrcc-gallery-style-pick';
|
||||
|
||||
type StyleId =
|
||||
| 'flame-forum'
|
||||
| 'memory-lane'
|
||||
| 'face-challenge'
|
||||
| 'reunion-pulse'
|
||||
| 'story-shelves'
|
||||
| 'soft-salon'
|
||||
| 'night-gallery'
|
||||
| 'digest-desk'
|
||||
| 'cluster-lab';
|
||||
|
||||
type StyleOption = {
|
||||
id: StyleId;
|
||||
name: string;
|
||||
tagline: string;
|
||||
whyComeBack: string;
|
||||
palette: string[];
|
||||
fonts: string;
|
||||
mood: string;
|
||||
previewClass: string;
|
||||
accentClass: string;
|
||||
sampleTitle: string;
|
||||
sampleSub: string;
|
||||
};
|
||||
|
||||
const STYLES: StyleOption[] = [
|
||||
{
|
||||
id: 'flame-forum',
|
||||
name: 'Flame & Forum',
|
||||
tagline: 'Community board energy',
|
||||
whyComeBack: 'See who tagged faces this week and jump into open unknowns.',
|
||||
palette: ['#0B1F4B', '#1E40AF', '#F5C518', '#F8FAFC'],
|
||||
fonts: 'Display: Manrope · Body: Source Sans 3',
|
||||
mood: 'Confident JRCC blue/gold, notice-board rhythm',
|
||||
previewClass: 'bg-[#0B1F4B] text-white',
|
||||
accentClass: 'bg-[#F5C518] text-[#0B1F4B]',
|
||||
sampleTitle: "Who's new this week",
|
||||
sampleSub: '12 faces waiting · 4 people tagged',
|
||||
},
|
||||
{
|
||||
id: 'memory-lane',
|
||||
name: 'Memory Lane',
|
||||
tagline: 'On this day, years ago',
|
||||
whyComeBack: 'Daily nostalgia: same date across decades of community photos.',
|
||||
palette: ['#2A2118', '#C4A484', '#F3EDE4', '#8B4513'],
|
||||
fonts: 'Display: Fraunces · Body: Literata',
|
||||
mood: 'Warm archival paper, soft film grain',
|
||||
previewClass: 'bg-[#F3EDE4] text-[#2A2118]',
|
||||
accentClass: 'bg-[#8B4513] text-[#F3EDE4]',
|
||||
sampleTitle: 'On this day · Aug 4',
|
||||
sampleSub: 'Photos from 1998 · 2007 · 2019',
|
||||
},
|
||||
{
|
||||
id: 'face-challenge',
|
||||
name: 'Face Challenge',
|
||||
tagline: 'Five unknowns a day',
|
||||
whyComeBack: 'A short daily streak: help name faces, climb a light scoreboard.',
|
||||
palette: ['#111827', '#22D3EE', '#F472B6', '#F9FAFB'],
|
||||
fonts: 'Display: Space Grotesk · Body: IBM Plex Sans',
|
||||
mood: 'Playful streak UI, big face crops, one-tap suggest',
|
||||
previewClass: 'bg-[#111827] text-white',
|
||||
accentClass: 'bg-[#22D3EE] text-[#111827]',
|
||||
sampleTitle: 'Daily challenge · 3 / 5',
|
||||
sampleSub: 'Streak: 6 days · Keep going',
|
||||
},
|
||||
{
|
||||
id: 'reunion-pulse',
|
||||
name: 'Reunion Pulse',
|
||||
tagline: 'Event mode progress',
|
||||
whyComeBack: 'Live “faces still unknown” bar after weddings, picnics, holidays.',
|
||||
palette: ['#7F1D1D', '#FCA5A5', '#FEF2F2', '#1F2937'],
|
||||
fonts: 'Display: Sora · Body: DM Sans',
|
||||
mood: 'Celebration meter, upload dump → clear the queue',
|
||||
previewClass: 'bg-[#FEF2F2] text-[#7F1D1D]',
|
||||
accentClass: 'bg-[#7F1D1D] text-white',
|
||||
sampleTitle: 'Purim 2026 album',
|
||||
sampleSub: '68% identified · 41 faces left',
|
||||
},
|
||||
{
|
||||
id: 'story-shelves',
|
||||
name: 'Story Shelves',
|
||||
tagline: 'People as timelines',
|
||||
whyComeBack: 'Open a person and scroll their life across events — not crop grids.',
|
||||
palette: ['#1C1917', '#A8A29E', '#FAFAF9', '#0EA5E9'],
|
||||
fonts: 'Display: Newsreader · Body: Karla',
|
||||
mood: 'Editorial shelves, chapter headings per decade',
|
||||
previewClass: 'bg-[#FAFAF9] text-[#1C1917]',
|
||||
accentClass: 'bg-[#0EA5E9] text-white',
|
||||
sampleTitle: 'Anna K. · Timeline',
|
||||
sampleSub: '1989 → Toronto · 112 photos',
|
||||
},
|
||||
{
|
||||
id: 'soft-salon',
|
||||
name: 'Soft Salon',
|
||||
tagline: 'Calm museum gallery',
|
||||
whyComeBack: 'Quiet browsing with generous space — invite family without noise.',
|
||||
palette: ['#F7F5F2', '#3F3A36', '#9A8F84', '#D6CFC6'],
|
||||
fonts: 'Display: Cormorant Garamond · Body: Outfit',
|
||||
mood: 'Breathing room, soft stone, restrained motion',
|
||||
previewClass: 'bg-[#F7F5F2] text-[#3F3A36]',
|
||||
accentClass: 'bg-[#3F3A36] text-[#F7F5F2]',
|
||||
sampleTitle: 'Collection',
|
||||
sampleSub: 'Search by people · seasons · places',
|
||||
},
|
||||
{
|
||||
id: 'night-gallery',
|
||||
name: 'Night Gallery',
|
||||
tagline: 'Dark-first cinema',
|
||||
whyComeBack: 'Evening browsing: luminous photos on deep navy, focus on faces.',
|
||||
palette: ['#020617', '#1E3A8A', '#E2E8F0', '#FBBF24'],
|
||||
fonts: 'Display: Syne · Body: Figtree',
|
||||
mood: 'Cinematic dark, gold spark accents',
|
||||
previewClass: 'bg-[#020617] text-[#E2E8F0]',
|
||||
accentClass: 'bg-[#FBBF24] text-[#020617]',
|
||||
sampleTitle: 'Evening gallery',
|
||||
sampleSub: 'Favorites · recently tagged',
|
||||
},
|
||||
{
|
||||
id: 'digest-desk',
|
||||
name: 'Digest Desk',
|
||||
tagline: 'Reasons to open mail',
|
||||
whyComeBack: 'Weekly digest preview in-app: 3 photos + one “who is this?”',
|
||||
palette: ['#0F172A', '#334155', '#F1F5F9', '#EA580C'],
|
||||
fonts: 'Display: Libre Baskerville · Body: Public Sans',
|
||||
mood: 'Editorial inbox, clip + caption cards',
|
||||
previewClass: 'bg-[#F1F5F9] text-[#0F172A]',
|
||||
accentClass: 'bg-[#EA580C] text-white',
|
||||
sampleTitle: 'This week’s digest',
|
||||
sampleSub: '3 memories · 1 mystery face',
|
||||
},
|
||||
{
|
||||
id: 'cluster-lab',
|
||||
name: 'Cluster Lab',
|
||||
tagline: 'Group lookalikes',
|
||||
whyComeBack: 'Name one stranger → cascade to a cluster of similar unknowns.',
|
||||
palette: ['#042F2E', '#14B8A6', '#CCFBF1', '#134E4A'],
|
||||
fonts: 'Display: Rubik · Body: Nunito Sans',
|
||||
mood: 'Lab grid of face clusters, teal precision',
|
||||
previewClass: 'bg-[#042F2E] text-[#CCFBF1]',
|
||||
accentClass: 'bg-[#14B8A6] text-[#042F2E]',
|
||||
sampleTitle: 'Cluster · 9 similar faces',
|
||||
sampleSub: 'Name one to link the rest',
|
||||
},
|
||||
];
|
||||
|
||||
function StylePreview({ style }: { style: StyleOption }) {
|
||||
return (
|
||||
<div className={cn('rounded-xl overflow-hidden border border-black/10 shadow-sm', style.previewClass)}>
|
||||
<div className="px-3 pt-3 pb-2 flex items-center justify-between gap-2">
|
||||
<div className="text-[10px] uppercase tracking-[0.14em] opacity-70">JRCC gallery</div>
|
||||
<span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full', style.accentClass)}>
|
||||
preview
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-3 pb-3">
|
||||
<div className="text-sm font-semibold leading-tight">{style.sampleTitle}</div>
|
||||
<div className="text-[11px] opacity-75 mt-0.5">{style.sampleSub}</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-square rounded-md bg-black/10 dark:bg-white/10 flex items-center justify-center text-[10px] opacity-60"
|
||||
>
|
||||
photo
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 flex gap-1.5">
|
||||
{style.palette.map((c) => (
|
||||
<span
|
||||
key={c}
|
||||
title={c}
|
||||
className="h-3 w-3 rounded-full border border-black/15"
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StylesPickerClient() {
|
||||
const [picked, setPicked] = useState<StyleId | null>(null);
|
||||
const [expandedIds, setExpandedIds] = useState<Set<StyleId>>(() => new Set());
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem(STORAGE_KEY) as StyleId | null;
|
||||
if (saved && STYLES.some((s) => s.id === saved)) {
|
||||
setPicked(saved);
|
||||
setExpandedIds(new Set([saved]));
|
||||
} else {
|
||||
// Open the three NEW styles so the exciting options are visible immediately
|
||||
setExpandedIds(
|
||||
new Set(STYLES.filter((s) => s.isNew).map((s) => s.id))
|
||||
);
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
@@ -207,15 +34,28 @@ export default function StylesPickerClient() {
|
||||
[picked]
|
||||
);
|
||||
|
||||
const toggleExpanded = (id: StyleId) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const expandAll = () => setExpandedIds(new Set(STYLES.map((s) => s.id)));
|
||||
const collapseAll = () => setExpandedIds(new Set());
|
||||
|
||||
const choose = (id: StyleId) => {
|
||||
setPicked(id);
|
||||
setExpandedIds((prev) => new Set(prev).add(id));
|
||||
window.localStorage.setItem(STORAGE_KEY, id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between gap-3">
|
||||
<div className="mx-auto max-w-7xl px-4 py-3 flex items-center justify-between gap-3">
|
||||
<Link href="/" aria-label="JRCC home" className="min-w-0">
|
||||
<JrccLogo heightClassName="h-10" />
|
||||
</Link>
|
||||
@@ -231,23 +71,40 @@ export default function StylesPickerClient() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main-content" className="mx-auto max-w-6xl px-4 py-8">
|
||||
<div className="max-w-2xl">
|
||||
<main id="main-content" className="mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground mb-2">
|
||||
Design lab · pick one
|
||||
Design lab · 12 directions
|
||||
</p>
|
||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight">
|
||||
Nine styles for a gallery people return to
|
||||
See each style on every page
|
||||
</h1>
|
||||
<p className="mt-3 text-muted-foreground leading-relaxed">
|
||||
Each card is a direction for more excitement and habit — not a full rebuild yet.
|
||||
Toggle light/dark above to check the JRCC logos. Tap a style to save your pick
|
||||
(stored in this browser only).
|
||||
Not just swatches — each direction shows Home, Photo, Search, Identify, Upload, and
|
||||
Login. Expand a style, scroll the mockups, then tap <strong>Pick this style</strong>.
|
||||
Three new high-energy options are marked <span className="text-foreground font-medium">NEW</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={expandAll}
|
||||
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted"
|
||||
>
|
||||
Expand all page mockups
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={collapseAll}
|
||||
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted"
|
||||
>
|
||||
Collapse all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hydrated && selected && (
|
||||
<div className="mt-6 rounded-xl border bg-card p-4 flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
||||
<div className="mt-4 rounded-xl border bg-card p-4 flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-muted-foreground">Your pick</div>
|
||||
<div className="font-semibold text-lg">{selected.name}</div>
|
||||
@@ -266,58 +123,109 @@ export default function StylesPickerClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mt-8 space-y-4">
|
||||
{STYLES.map((style, index) => {
|
||||
const isSelected = picked === style.id;
|
||||
const isOpen = expandedIds.has(style.id);
|
||||
return (
|
||||
<button
|
||||
<section
|
||||
key={style.id}
|
||||
type="button"
|
||||
onClick={() => choose(style.id)}
|
||||
className={cn(
|
||||
'text-left rounded-2xl border p-3 transition-all hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
isSelected ? 'border-primary ring-2 ring-primary/40' : 'border-border'
|
||||
'rounded-2xl border transition-shadow',
|
||||
isSelected ? 'border-primary ring-2 ring-primary/30 shadow-md' : 'border-border',
|
||||
isOpen && 'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-2 mb-2 px-0.5">
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<span className="text-xs font-medium text-primary">Selected</span>
|
||||
)}
|
||||
<div className="p-4 sm:p-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{style.isNew && (
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-700 dark:text-amber-300">
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
{isSelected && (
|
||||
<span className="text-xs font-medium text-primary">Selected</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold tracking-tight">{style.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">{style.tagline}</p>
|
||||
<p className="mt-2 text-sm leading-snug max-w-2xl">{style.whyComeBack}</p>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">
|
||||
{style.fonts} · {style.mood}
|
||||
</p>
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
{[
|
||||
style.tokens.bg,
|
||||
style.tokens.accent,
|
||||
style.tokens.photoB,
|
||||
style.tokens.photoC,
|
||||
].map((c) => (
|
||||
<span
|
||||
key={c}
|
||||
title={c}
|
||||
className="h-3.5 w-3.5 rounded-full border border-black/15"
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleExpanded(style.id)}
|
||||
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? 'Hide page mockups' : 'Show all pages'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => choose(style.id)}
|
||||
className={cn(
|
||||
'text-sm px-3 py-1.5 rounded-md font-medium',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-foreground text-background hover:opacity-90'
|
||||
)}
|
||||
>
|
||||
{isSelected ? 'Picked' : 'Pick this style'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<StylePreview style={style} />
|
||||
<div className="mt-3 px-0.5">
|
||||
<h2 className="font-semibold text-base">{style.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">{style.tagline}</p>
|
||||
<p className="mt-2 text-sm leading-snug">{style.whyComeBack}</p>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">{style.fonts}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{style.mood}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="px-4 sm:px-5 pb-5 border-t bg-muted/20">
|
||||
<p className="pt-4 pb-3 text-xs text-muted-foreground">
|
||||
How <strong className="text-foreground">{style.name}</strong> would feel on
|
||||
each screen (wireframe mockups — not live theme yet).
|
||||
</p>
|
||||
<StylePageGallery style={style} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<section className="mt-12 mb-8 rounded-2xl border p-5 bg-muted/30">
|
||||
<h2 className="font-semibold text-lg">Logo usage (this pass)</h2>
|
||||
<h2 className="font-semibold text-lg">Logo usage (already live)</h2>
|
||||
<ul className="mt-2 text-sm text-muted-foreground space-y-1.5 list-disc pl-5">
|
||||
<li>
|
||||
<strong className="text-foreground">Light mode header:</strong> classic black
|
||||
bilingual JRCC mark (`jrcc-logo-light.png`)
|
||||
<strong className="text-foreground">Light header:</strong> black bilingual JRCC
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Dark mode header:</strong> white bilingual
|
||||
mark on transparent (`jrcc-logo-dark.png`)
|
||||
<strong className="text-foreground">Dark header:</strong> white bilingual, transparent
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Photo watermark:</strong> translucent JRCC
|
||||
overlay for guest downloads (`jrcc-watermark.png`)
|
||||
<strong className="text-foreground">Guest watermark:</strong> translucent JRCC overlay
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
Tell us which style number wins and we'll implement that direction next.
|
||||
Tell us the winning style number (01–12) and we'll implement that direction next.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -3,9 +3,20 @@ import StylesPickerClient from './StylesPickerClient';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'JRCC gallery — style picker',
|
||||
description: 'Pick a visual direction for the JRCC photo gallery redesign.',
|
||||
description:
|
||||
'Compare 12 redesign directions with page mockups for Home, Photo, Search, Identify, Upload, and Login.',
|
||||
};
|
||||
|
||||
export default function StylesPage() {
|
||||
return <StylesPickerClient />;
|
||||
return (
|
||||
<>
|
||||
{/* Preview fonts for style mockups only */}
|
||||
{/* eslint-disable-next-line @next/next/no-page-custom-font */}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:wght@400;700&family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700&family=Chakra+Petch:wght@400;600;700&family=Cormorant+Garamond:wght@600&family=DM+Sans:wght@400;600&family=Figtree:wght@400;600&family=Fraunces:opsz,wght@9..144,600&family=IBM+Plex+Sans:wght@400;600&family=Instrument+Serif&family=Karla:wght@400;600&family=Libre+Baskerville:wght@700&family=Literata:opsz,wght@7..72,400;7..72,600&family=Manrope:wght@600;700&family=Newsreader:opsz,wght@6..72,600&family=Nunito+Sans:wght@400;700&family=Outfit:wght@400;600&family=Press+Start+2P&family=Public+Sans:wght@400;600&family=Rubik:wght@500;700&family=Sora:wght@600&family=Source+Sans+3:wght@400;600&family=Space+Grotesk:wght@500;700&family=Syne:wght@700&display=swap"
|
||||
/>
|
||||
<StylesPickerClient />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
export type StyleId =
|
||||
| 'flame-forum'
|
||||
| 'memory-lane'
|
||||
| 'face-challenge'
|
||||
| 'reunion-pulse'
|
||||
| 'story-shelves'
|
||||
| 'soft-salon'
|
||||
| 'night-gallery'
|
||||
| 'digest-desk'
|
||||
| 'cluster-lab'
|
||||
| 'matchmaker-arcade'
|
||||
| 'parade-mosaic'
|
||||
| 'orbit-kin';
|
||||
|
||||
export type StyleTokens = {
|
||||
bg: string;
|
||||
surface: string;
|
||||
text: string;
|
||||
muted: string;
|
||||
accent: string;
|
||||
accentText: string;
|
||||
border: string;
|
||||
chip: string;
|
||||
photoA: string;
|
||||
photoB: string;
|
||||
photoC: string;
|
||||
radius: string;
|
||||
display: string;
|
||||
body: string;
|
||||
density: 'airy' | 'cozy' | 'dense';
|
||||
vibe: 'editorial' | 'playful' | 'cinematic' | 'festive' | 'lab' | 'arcade';
|
||||
};
|
||||
|
||||
export type StyleOption = {
|
||||
id: StyleId;
|
||||
name: string;
|
||||
tagline: string;
|
||||
whyComeBack: string;
|
||||
fonts: string;
|
||||
mood: string;
|
||||
isNew?: boolean;
|
||||
tokens: StyleTokens;
|
||||
};
|
||||
|
||||
export const PAGE_TYPES = [
|
||||
{ id: 'home', label: 'Home / browse' },
|
||||
{ id: 'photo', label: 'Photo viewer' },
|
||||
{ id: 'search', label: 'Search' },
|
||||
{ id: 'identify', label: 'Identify face' },
|
||||
{ id: 'upload', label: 'Upload' },
|
||||
{ id: 'login', label: 'Login' },
|
||||
] as const;
|
||||
|
||||
export type PageTypeId = (typeof PAGE_TYPES)[number]['id'];
|
||||
|
||||
export const STYLES: StyleOption[] = [
|
||||
{
|
||||
id: 'flame-forum',
|
||||
name: 'Flame & Forum',
|
||||
tagline: 'Community board energy',
|
||||
whyComeBack: 'See who tagged faces this week and jump into open unknowns.',
|
||||
fonts: 'Display: Manrope · Body: Source Sans 3',
|
||||
mood: 'Confident JRCC blue/gold, notice-board rhythm',
|
||||
tokens: {
|
||||
bg: '#0B1F4B',
|
||||
surface: '#132A5C',
|
||||
text: '#F8FAFC',
|
||||
muted: '#93C5FD',
|
||||
accent: '#F5C518',
|
||||
accentText: '#0B1F4B',
|
||||
border: 'rgba(245,197,24,0.35)',
|
||||
chip: '#1E40AF',
|
||||
photoA: '#1E3A8A',
|
||||
photoB: '#2563EB',
|
||||
photoC: '#FBBF24',
|
||||
radius: '10px',
|
||||
display: '"Manrope", system-ui, sans-serif',
|
||||
body: '"Source Sans 3", system-ui, sans-serif',
|
||||
density: 'cozy',
|
||||
vibe: 'editorial',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory-lane',
|
||||
name: 'Memory Lane',
|
||||
tagline: 'On this day, years ago',
|
||||
whyComeBack: 'Daily nostalgia: same date across decades of community photos.',
|
||||
fonts: 'Display: Fraunces · Body: Literata',
|
||||
mood: 'Warm archival paper, soft film grain',
|
||||
tokens: {
|
||||
bg: '#F3EDE4',
|
||||
surface: '#EDE4D6',
|
||||
text: '#2A2118',
|
||||
muted: '#7A6550',
|
||||
accent: '#8B4513',
|
||||
accentText: '#F3EDE4',
|
||||
border: 'rgba(42,33,24,0.18)',
|
||||
chip: '#C4A484',
|
||||
photoA: '#D6C3A8',
|
||||
photoB: '#B8956E',
|
||||
photoC: '#8B6914',
|
||||
radius: '4px',
|
||||
display: '"Fraunces", Georgia, serif',
|
||||
body: '"Literata", Georgia, serif',
|
||||
density: 'airy',
|
||||
vibe: 'editorial',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'face-challenge',
|
||||
name: 'Face Challenge',
|
||||
tagline: 'Five unknowns a day',
|
||||
whyComeBack: 'A short daily streak: help name faces, climb a light scoreboard.',
|
||||
fonts: 'Display: Space Grotesk · Body: IBM Plex Sans',
|
||||
mood: 'Playful streak UI, big face crops, one-tap suggest',
|
||||
tokens: {
|
||||
bg: '#111827',
|
||||
surface: '#1F2937',
|
||||
text: '#F9FAFB',
|
||||
muted: '#9CA3AF',
|
||||
accent: '#22D3EE',
|
||||
accentText: '#111827',
|
||||
border: 'rgba(34,211,238,0.35)',
|
||||
chip: '#F472B6',
|
||||
photoA: '#374151',
|
||||
photoB: '#0891B2',
|
||||
photoC: '#DB2777',
|
||||
radius: '16px',
|
||||
display: '"Space Grotesk", system-ui, sans-serif',
|
||||
body: '"IBM Plex Sans", system-ui, sans-serif',
|
||||
density: 'cozy',
|
||||
vibe: 'playful',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'reunion-pulse',
|
||||
name: 'Reunion Pulse',
|
||||
tagline: 'Event mode progress',
|
||||
whyComeBack: 'Live “faces still unknown” bar after weddings, picnics, holidays.',
|
||||
fonts: 'Display: Sora · Body: DM Sans',
|
||||
mood: 'Celebration meter, upload dump → clear the queue',
|
||||
tokens: {
|
||||
bg: '#FEF2F2',
|
||||
surface: '#FFFFFF',
|
||||
text: '#7F1D1D',
|
||||
muted: '#B91C1C',
|
||||
accent: '#7F1D1D',
|
||||
accentText: '#FEF2F2',
|
||||
border: 'rgba(127,29,29,0.2)',
|
||||
chip: '#FCA5A5',
|
||||
photoA: '#FECACA',
|
||||
photoB: '#F87171',
|
||||
photoC: '#991B1B',
|
||||
radius: '14px',
|
||||
display: '"Sora", system-ui, sans-serif',
|
||||
body: '"DM Sans", system-ui, sans-serif',
|
||||
density: 'cozy',
|
||||
vibe: 'festive',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'story-shelves',
|
||||
name: 'Story Shelves',
|
||||
tagline: 'People as timelines',
|
||||
whyComeBack: 'Open a person and scroll their life across events — not crop grids.',
|
||||
fonts: 'Display: Newsreader · Body: Karla',
|
||||
mood: 'Editorial shelves, chapter headings per decade',
|
||||
tokens: {
|
||||
bg: '#FAFAF9',
|
||||
surface: '#FFFFFF',
|
||||
text: '#1C1917',
|
||||
muted: '#78716C',
|
||||
accent: '#0EA5E9',
|
||||
accentText: '#FFFFFF',
|
||||
border: 'rgba(28,25,23,0.12)',
|
||||
chip: '#E7E5E4',
|
||||
photoA: '#D6D3D1',
|
||||
photoB: '#A8A29E',
|
||||
photoC: '#0EA5E9',
|
||||
radius: '2px',
|
||||
display: '"Newsreader", Georgia, serif',
|
||||
body: '"Karla", system-ui, sans-serif',
|
||||
density: 'airy',
|
||||
vibe: 'editorial',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'soft-salon',
|
||||
name: 'Soft Salon',
|
||||
tagline: 'Calm museum gallery',
|
||||
whyComeBack: 'Quiet browsing with generous space — invite family without noise.',
|
||||
fonts: 'Display: Cormorant Garamond · Body: Outfit',
|
||||
mood: 'Breathing room, soft stone, restrained motion',
|
||||
tokens: {
|
||||
bg: '#F7F5F2',
|
||||
surface: '#FFFFFF',
|
||||
text: '#3F3A36',
|
||||
muted: '#9A8F84',
|
||||
accent: '#3F3A36',
|
||||
accentText: '#F7F5F2',
|
||||
border: 'rgba(63,58,54,0.14)',
|
||||
chip: '#D6CFC6',
|
||||
photoA: '#E8E2DA',
|
||||
photoB: '#C9BFB4',
|
||||
photoC: '#8A8076',
|
||||
radius: '20px',
|
||||
display: '"Cormorant Garamond", Georgia, serif',
|
||||
body: '"Outfit", system-ui, sans-serif',
|
||||
density: 'airy',
|
||||
vibe: 'editorial',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'night-gallery',
|
||||
name: 'Night Gallery',
|
||||
tagline: 'Dark-first cinema',
|
||||
whyComeBack: 'Evening browsing: luminous photos on deep navy, focus on faces.',
|
||||
fonts: 'Display: Syne · Body: Figtree',
|
||||
mood: 'Cinematic dark, gold spark accents',
|
||||
tokens: {
|
||||
bg: '#020617',
|
||||
surface: '#0F172A',
|
||||
text: '#E2E8F0',
|
||||
muted: '#94A3B8',
|
||||
accent: '#FBBF24',
|
||||
accentText: '#020617',
|
||||
border: 'rgba(251,191,36,0.28)',
|
||||
chip: '#1E3A8A',
|
||||
photoA: '#1E293B',
|
||||
photoB: '#1E3A8A',
|
||||
photoC: '#B45309',
|
||||
radius: '12px',
|
||||
display: '"Syne", system-ui, sans-serif',
|
||||
body: '"Figtree", system-ui, sans-serif',
|
||||
density: 'dense',
|
||||
vibe: 'cinematic',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'digest-desk',
|
||||
name: 'Digest Desk',
|
||||
tagline: 'Reasons to open mail',
|
||||
whyComeBack: 'Weekly digest preview in-app: 3 photos + one “who is this?”',
|
||||
fonts: 'Display: Libre Baskerville · Body: Public Sans',
|
||||
mood: 'Editorial inbox, clip + caption cards',
|
||||
tokens: {
|
||||
bg: '#F1F5F9',
|
||||
surface: '#FFFFFF',
|
||||
text: '#0F172A',
|
||||
muted: '#64748B',
|
||||
accent: '#EA580C',
|
||||
accentText: '#FFFFFF',
|
||||
border: 'rgba(15,23,42,0.12)',
|
||||
chip: '#E2E8F0',
|
||||
photoA: '#CBD5E1',
|
||||
photoB: '#94A3B8',
|
||||
photoC: '#EA580C',
|
||||
radius: '8px',
|
||||
display: '"Libre Baskerville", Georgia, serif',
|
||||
body: '"Public Sans", system-ui, sans-serif',
|
||||
density: 'cozy',
|
||||
vibe: 'editorial',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cluster-lab',
|
||||
name: 'Cluster Lab',
|
||||
tagline: 'Group lookalikes',
|
||||
whyComeBack: 'Name one stranger → cascade to a cluster of similar unknowns.',
|
||||
fonts: 'Display: Rubik · Body: Nunito Sans',
|
||||
mood: 'Lab grid of face clusters, teal precision',
|
||||
tokens: {
|
||||
bg: '#042F2E',
|
||||
surface: '#134E4A',
|
||||
text: '#CCFBF1',
|
||||
muted: '#5EEAD4',
|
||||
accent: '#14B8A6',
|
||||
accentText: '#042F2E',
|
||||
border: 'rgba(20,184,166,0.4)',
|
||||
chip: '#0F766E',
|
||||
photoA: '#115E59',
|
||||
photoB: '#14B8A6',
|
||||
photoC: '#99F6E4',
|
||||
radius: '10px',
|
||||
display: '"Rubik", system-ui, sans-serif',
|
||||
body: '"Nunito Sans", system-ui, sans-serif',
|
||||
density: 'dense',
|
||||
vibe: 'lab',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'matchmaker-arcade',
|
||||
name: 'Matchmaker Arcade',
|
||||
tagline: 'Name faces like a high-score game',
|
||||
whyComeBack: 'Combo multipliers, boss “mystery faces,” and weekly JRCC leaderboards.',
|
||||
fonts: 'Display: Press Start 2P · Body: Chakra Petch',
|
||||
mood: 'Cabinet glow, chunky CTAs, score pops on every ID',
|
||||
isNew: true,
|
||||
tokens: {
|
||||
bg: '#1A0533',
|
||||
surface: '#2D0B55',
|
||||
text: '#F5E9FF',
|
||||
muted: '#C4B5FD',
|
||||
accent: '#A3E635',
|
||||
accentText: '#14532D',
|
||||
border: 'rgba(163,230,53,0.45)',
|
||||
chip: '#7C3AED',
|
||||
photoA: '#4C1D95',
|
||||
photoB: '#DB2777',
|
||||
photoC: '#A3E635',
|
||||
radius: '6px',
|
||||
display: '"Press Start 2P", monospace',
|
||||
body: '"Chakra Petch", system-ui, sans-serif',
|
||||
density: 'dense',
|
||||
vibe: 'arcade',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'parade-mosaic',
|
||||
name: 'Parade Mosaic',
|
||||
tagline: 'Event dump → living mural',
|
||||
whyComeBack: 'After Purim/picnic uploads, the wall animates as faces get named.',
|
||||
fonts: 'Display: Bricolage Grotesque · Body: Atkinson Hyperlegible',
|
||||
mood: 'Festival stripes, diagonal banners, mosaic tiles that unlock',
|
||||
isNew: true,
|
||||
tokens: {
|
||||
bg: '#FFF7ED',
|
||||
surface: '#FFEDD5',
|
||||
text: '#9A3412',
|
||||
muted: '#C2410C',
|
||||
accent: '#0369A1',
|
||||
accentText: '#F0F9FF',
|
||||
border: 'rgba(154,52,18,0.22)',
|
||||
chip: '#FDBA74',
|
||||
photoA: '#FB923C',
|
||||
photoB: '#0EA5E9',
|
||||
photoC: '#E11D48',
|
||||
radius: '18px',
|
||||
display: '"Bricolage Grotesque", system-ui, sans-serif',
|
||||
body: '"Atkinson Hyperlegible", system-ui, sans-serif',
|
||||
density: 'cozy',
|
||||
vibe: 'festive',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'orbit-kin',
|
||||
name: 'Orbit Kin',
|
||||
tagline: 'Family as a constellation',
|
||||
whyComeBack: 'Tap a person → orbit their relatives & event clusters across decades.',
|
||||
fonts: 'Display: Instrument Serif · Body: Geist',
|
||||
mood: 'Ink-sky map, copper trails, quiet wow for multi-gen families',
|
||||
isNew: true,
|
||||
tokens: {
|
||||
bg: '#071018',
|
||||
surface: '#0E1A24',
|
||||
text: '#E8F1F5',
|
||||
muted: '#7A93A3',
|
||||
accent: '#D4A373',
|
||||
accentText: '#1C1208',
|
||||
border: 'rgba(212,163,115,0.35)',
|
||||
chip: '#1E3A47',
|
||||
photoA: '#16303C',
|
||||
photoB: '#2A5160',
|
||||
photoC: '#D4A373',
|
||||
radius: '999px',
|
||||
display: '"Instrument Serif", Georgia, serif',
|
||||
body: '"Geist", system-ui, sans-serif',
|
||||
density: 'airy',
|
||||
vibe: 'cinematic',
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user