Make Jerusalem Stone / Kotel styles look like Western Wall masonry.
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 30s
CI / python-lint (pull_request) Successful in 31s
CI / secret-scan (pull_request) Successful in 38s
CI / viewer-unit (pull_request) Successful in 1m44s
CI / admin-unit (pull_request) Successful in 1m59s
CI / e2e (pull_request) Successful in 3m2s

Golden limestone courses, mortar joints, mottling, and staggered block mockups instead of flat beige.
This commit is contained in:
2026-08-04 14:15:51 -04:00
parent fbb5926bc1
commit 37aa3c0bde
2 changed files with 142 additions and 47 deletions
+107 -16
View File
@@ -4,26 +4,53 @@ import type { CSSProperties, ReactNode } from 'react';
import { cn } from '@/lib/utils';
import type { PageTypeId, StyleOption, StyleTokens } from './styleOptions';
/** CSS Kotel / Herodian limestone courses (staggered blocks + mortar). */
function kotelMasonry(base: string, mortar = 'rgba(46,41,32,0.28)'): string {
const course = 22;
const joint = 2;
const row = course + joint;
return [
// horizontal mortar between courses
`repeating-linear-gradient(0deg, transparent 0 ${course}px, ${mortar} ${course}px ${row}px)`,
// vertical joints — even courses
`repeating-linear-gradient(90deg, transparent 0 38px, ${mortar} 38px 40px)`,
// soft stone mottling
`radial-gradient(ellipse 80% 60% at 20% 30%, rgba(255,255,255,0.18), transparent 55%)`,
`radial-gradient(ellipse 70% 50% at 75% 70%, rgba(0,0,0,0.08), transparent 50%)`,
`linear-gradient(180deg, ${base}ee, ${base})`,
].join(', ');
}
function shellStyle(t: StyleTokens): CSSProperties {
const isKotel = t.texture === 'kotel';
return {
background: t.bg,
backgroundColor: t.bg,
backgroundImage: isKotel ? kotelMasonry(t.bg) : undefined,
color: t.text,
fontFamily: t.body,
borderRadius: t.radius === '999px' ? '16px' : t.radius,
border: `1px solid ${t.border}`,
boxShadow: isKotel ? 'inset 0 0 40px rgba(46,41,32,0.12)' : undefined,
};
}
function HeaderBar({ t, title }: { t: StyleTokens; title: string }) {
const isKotel = t.texture === 'kotel';
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 }}
style={{
borderColor: t.border,
backgroundColor: t.surface,
backgroundImage: isKotel
? kotelMasonry(t.surface, 'rgba(46,41,32,0.22)')
: undefined,
}}
>
<div className="min-w-0">
<div
className="text-[8px] font-bold tracking-wide truncate"
style={{ fontFamily: t.display, color: t.accent }}
style={{ fontFamily: t.display, color: isKotel ? t.text : t.accent }}
>
JRCC
</div>
@@ -59,13 +86,33 @@ function PhotoTile({
tall?: boolean;
}) {
const bg = tone === 'A' ? t.photoA : tone === 'B' ? t.photoB : t.photoC;
const isKotel = t.texture === 'kotel';
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,
backgroundColor: bg,
backgroundImage: isKotel
? [
// single “block” with chisel edge
`linear-gradient(135deg, rgba(255,255,255,0.22) 0%, transparent 42%)`,
`linear-gradient(225deg, rgba(0,0,0,0.14) 0%, transparent 40%)`,
`repeating-linear-gradient(0deg, transparent 0 10px, rgba(46,41,32,0.12) 10px 11px)`,
`linear-gradient(180deg, ${bg}f0, ${bg})`,
].join(', ')
: undefined,
borderRadius: isKotel
? '2px'
: t.vibe === 'arcade'
? '4px'
: t.radius === '999px'
? '12px'
: t.radius,
boxShadow: isKotel
? `inset 0 0 0 1px rgba(46,41,32,0.25), 1px 1px 0 rgba(255,255,255,0.15)`
: t.vibe === 'cinematic'
? `0 0 0 1px ${t.border}`
: undefined,
}}
>
{t.vibe === 'festive' && (
@@ -76,9 +123,21 @@ function PhotoTile({
}}
/>
)}
{isKotel && (
<div
className="absolute inset-x-1 top-1 bottom-1 opacity-30 pointer-events-none"
style={{
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.4 0 0 0 0 0.35 0 0 0 0 0.25 0 0 0 0.35 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>`
)}")`,
backgroundSize: '40px 40px',
mixBlendMode: 'multiply',
}}
/>
)}
{label && (
<span
className="m-1 text-[7px] px-1 py-0.5 font-medium"
className="m-1 text-[7px] px-1 py-0.5 font-medium relative z-[1]"
style={{
background: `${t.surface}cc`,
color: t.text,
@@ -94,18 +153,21 @@ function PhotoTile({
function MockHome({ t }: { t: StyleTokens }) {
const gap = t.density === 'airy' ? 'gap-2' : t.density === 'dense' ? 'gap-1' : 'gap-1.5';
const isKotel = t.texture === 'kotel';
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'}
{isKotel
? 'At the Wall · faces & notes'
: 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
@@ -140,6 +202,22 @@ function MockHome({ t }: { t: StyleTokens }) {
<line x1="48%" y1="22%" x2="48%" y2="62%" stroke={t.accent} strokeWidth="0.5" />
</svg>
</div>
) : isKotel ? (
<div className="px-2.5 pb-2.5 space-y-1">
{/* masonry row: large Herodian course */}
<div className="grid grid-cols-3 gap-1">
<PhotoTile t={t} tone="A" label="note" />
<PhotoTile t={t} tone="B" />
<PhotoTile t={t} tone="C" />
</div>
{/* staggered second course */}
<div className="grid grid-cols-4 gap-1 pl-3">
<PhotoTile t={t} tone="B" />
<PhotoTile t={t} tone="A" />
<PhotoTile t={t} tone="C" />
<PhotoTile t={t} tone="B" />
</div>
</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} />
@@ -150,7 +228,7 @@ function MockHome({ t }: { t: StyleTokens }) {
<PhotoTile t={t} tone="A" />
</div>
)}
{t.vibe === 'playful' || t.vibe === 'arcade' ? (
{(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={{
@@ -162,7 +240,20 @@ function MockHome({ t }: { t: StyleTokens }) {
<span>{t.vibe === 'arcade' ? 'START DAILY MATCH' : 'Daily challenge · 3/5'}</span>
<span></span>
</div>
) : null}
)}
{isKotel && (
<div
className="mx-2.5 mb-2.5 px-2 py-1.5 text-[9px] border"
style={{
borderColor: t.border,
background: `${t.surface}cc`,
borderRadius: '2px',
color: t.text,
}}
>
Place a name · like a note in the Wall
</div>
)}
</div>
);
}
+35 -31
View File
@@ -35,6 +35,8 @@ export type StyleTokens = {
body: string;
density: 'airy' | 'cozy' | 'dense';
vibe: 'editorial' | 'playful' | 'cinematic' | 'festive' | 'lab' | 'arcade';
/** Optional surface treatment for mockups (e.g. Kotel masonry). */
texture?: 'kotel' | 'none';
};
export type StyleOption = {
@@ -375,28 +377,29 @@ export const STYLES: StyleOption[] = [
{
id: 'jerusalem-stone',
name: 'Jerusalem Stone',
tagline: 'Limestone light, quiet dignity',
whyComeBack: 'Browse like walking a sunlit street — warm stone, soft shadows, faces first.',
tagline: 'Kotel courses — golden limestone',
whyComeBack: 'Browse against Herodian block courses: warm gold stone, soft mortar, faces first.',
fonts: 'Display: Frank Ruhl Libre · Body: Assistant',
mood: 'אבן ירושלמית — modern, calm, not flashy',
mood: 'Western Wall masonry — sunlit gold limestone, not flat beige',
isNew: true,
tokens: {
bg: '#EDE6DC',
surface: '#F7F3EC',
text: '#3D3429',
muted: '#8A7E6E',
accent: '#6B5A45',
accentText: '#F7F3EC',
border: 'rgba(61,52,41,0.14)',
chip: '#D9D0C2',
photoA: '#C9BBA8',
photoB: '#A89880',
photoC: '#7A6A55',
radius: '8px',
bg: '#C9B896',
surface: '#D8C9A0',
text: '#2E2920',
muted: '#6E6354',
accent: '#8B7355',
accentText: '#F5EFE3',
border: 'rgba(46,41,32,0.22)',
chip: '#B8A888',
photoA: '#B5A280',
photoB: '#9C8A68',
photoC: '#7A6A50',
radius: '3px',
display: '"Frank Ruhl Libre", Georgia, serif',
body: '"Assistant", system-ui, sans-serif',
density: 'airy',
density: 'cozy',
vibe: 'editorial',
texture: 'kotel',
},
},
{
@@ -429,28 +432,29 @@ export const STYLES: StyleOption[] = [
{
id: 'kotel-quiet',
name: 'Kotel Quiet',
tagline: 'Stone layers, soft reverence',
whyComeBack: 'Slow looking — notes beside photos, names offered gently, no scoreboard.',
tagline: 'Western Wall — notes in the cracks',
whyComeBack: 'Slow looking against massive courses: paper notes, soft names, no scoreboard.',
fonts: 'Display: David Libre · Body: Heebo',
mood: 'Weathered limestone + deep charcoal; prayerful, not theatrical',
mood: 'Heavy Herodian blocks, deep mortar, prayerful shadow',
isNew: true,
tokens: {
bg: '#F0EBE3',
surface: '#E7E0D5',
text: '#2C2A26',
muted: '#6E675C',
accent: '#4A453C',
accentText: '#F0EBE3',
border: 'rgba(44,42,38,0.16)',
chip: '#D4CDC2',
photoA: '#B8AFA1',
photoB: '#8E8578',
photoC: '#5C564C',
bg: '#B5A280',
surface: '#C9B896',
text: '#1F1C17',
muted: '#5C5346',
accent: '#4A4034',
accentText: '#EDE4D4',
border: 'rgba(31,28,23,0.28)',
chip: '#A09070',
photoA: '#9C8A68',
photoB: '#7A6A50',
photoC: '#5A4C3A',
radius: '2px',
display: '"David Libre", Georgia, serif',
body: '"Heebo", system-ui, sans-serif',
density: 'airy',
density: 'cozy',
vibe: 'editorial',
texture: 'kotel',
},
},
{