Fix layout + misc

This commit is contained in:
Thomas Fransolet 2026-05-07 16:50:19 +02:00
parent 57ee68e72b
commit 67d8ce94d6
21 changed files with 284 additions and 86 deletions

View File

@ -23,12 +23,14 @@ export default async function HomePage({
// The featured event is rendered inside its parent configuration's context
const featuredConfigId = featuredEvent?.configurationId
const languages = [...new Set(configurations.flatMap((c) => c.languages ?? []))]
return (
<>
{featuredEvent && featuredConfigId && (
<FeaturedEvent event={featuredEvent} slug={slug} configurationId={featuredConfigId} />
)}
<ConfigurationGrid configurations={configurations} slug={slug} />
<ConfigurationGrid configurations={configurations} slug={slug} languages={languages} />
<QRScannerButton slug={slug} />
</>
)

View File

@ -1,33 +1,28 @@
'use client'
import { useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { useVisitor } from '@/context/VisitorContext'
import { t, tPlain } from '@/lib/i18n'
import type { ConfigurationDTO } from '@/lib/api/types'
import AppBar from '@/components/ui/AppBar'
interface Props {
configurations: ConfigurationDTO[]
slug: string
languages: string[]
}
export default function ConfigurationGrid({ configurations, slug }: Props) {
const { language } = useVisitor()
export default function ConfigurationGrid({ configurations, slug, languages }: Props) {
const { language, setAvailableLanguages } = useVisitor()
const active = configurations.filter((c) => !c.isOffline)
useEffect(() => { setAvailableLanguages(languages) }, [languages])
return (
<main className="min-h-screen" style={{ background: 'var(--color-background)' }}>
<header
className="flex items-center justify-center px-4 py-4"
style={{
background: 'linear-gradient(135deg, var(--color-primary), var(--color-secondary))',
minHeight: 56,
}}
>
<span className="text-lg font-semibold" style={{ color: 'var(--color-on-primary)' }}>
MyInfoMate
</span>
</header>
<AppBar />
<div className="p-4 columns-2 gap-3">
{active.map((config) => (

View File

@ -26,7 +26,7 @@ export default function SectionList({
const [search, setSearch] = useState('')
const [searchNumber, setSearchNumber] = useState('')
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const filtered = sections.filter((s) => {
if (searchNumber) return (s.order ?? 0) + 1 === parseInt(searchNumber)

View File

@ -65,13 +65,15 @@ export default function AgendaSection({ section, configId, languages }: Props) {
const router = useRouter()
const events = section.agenda?.events ?? []
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const now = new Date()
const [selectedMonth, setSelectedMonth] = useState(now.getMonth())
const [selectedYear, setSelectedYear] = useState(now.getFullYear())
const [selected, setSelected] = useState<EventAgendaDTO | null>(null)
useEffect(() => { console.log('[Agenda] events:', events.map(e => ({ id: e.id, label: e.label, resourceId: e.resourceId, resource: e.resource }))) }, [events])
const filtered = events.filter((e) => {
if (!e.dateFrom) return false
const d = new Date(e.dateFrom)

View File

@ -1,8 +1,8 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import ResourceViewer from '@/components/ui/ResourceViewer'
import { useVisitor } from '@/context/VisitorContext'
import { t, tPlain } from '@/lib/i18n'
import type { SectionDTO } from '@/lib/api/types'
@ -27,7 +27,7 @@ export default function ArticleSection({ section, configId, languages }: Props)
const scrollRef = useRef<HTMLElement>(null)
const articleReadTrackedRef = useRef(false)
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
useEffect(() => {
const el = scrollRef.current
@ -140,13 +140,11 @@ function ImageCarousel({ contents, language }: { contents: NonNullable<SectionDT
return (
<div className="relative w-full" style={{ height: 240 }}>
{contents[index]?.resource?.url && (
<Image
src={contents[index].resource!.url!}
alt={t(contents[index].title, language)}
fill
className="object-cover"
sizes="100vw"
{contents[index]?.resource && (
<ResourceViewer
resource={contents[index].resource!}
alt={tPlain(contents[index].title, language)}
objectFit="cover"
/>
)}
{contents.length > 1 && (

View File

@ -43,7 +43,7 @@ export default function EventSection({ section, slug, configId, languages }: Pro
const { language, setAvailableLanguages } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const event = section.event
const programme = useMemo(

View File

@ -32,7 +32,7 @@ export default function GameSection({ section, configId, languages }: Props) {
const { language, setAvailableLanguages, instanceId } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const kind = detectKind(section.gameType)
const rows = Math.max(2, section.rows ?? 3)
@ -204,6 +204,16 @@ export default function GameSection({ section, configId, languages }: Props) {
open={showEnd}
title="Bravo !"
html={endMsg || 'Tu as gagné !'}
icon={
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ background: 'var(--color-primary-light)' }}
>
<svg width="32" height="32" viewBox="0 0 24 24" fill="var(--color-primary)">
<path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94A5.01 5.01 0 0 0 11 15.9V18H9v2h6v-2h-2v-2.1a5.01 5.01 0 0 0 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z" />
</svg>
</div>
}
onClose={restart}
primaryAction={{ label: 'Recommencer', onClick: restart }}
secondaryAction={{ label: 'Retour', onClick: () => router.back() }}

View File

@ -36,7 +36,7 @@ export default function MapSection({ section, configId, languages }: Props) {
const { language, setAvailableLanguages, instanceId } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const map = section.map
const hasData = !!map && (map.points?.length ?? 0) > 0

View File

@ -31,7 +31,7 @@ export default function MenuSection({ section, slug, configId, languages }: Prop
})
}
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const subsections = [...(section.menu?.sections ?? [])]
.filter((s) => s.isActive !== false)

View File

@ -18,7 +18,7 @@ export default function PdfSection({ section, slug, configId, languages }: Props
const { language, setAvailableLanguages } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const pdfs = [...(section.pdfs ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const [selectedIndex, setSelectedIndex] = useState(0)

View File

@ -21,7 +21,7 @@ export default function QuizSection({ section, configId, languages }: Props) {
const { language, setAvailableLanguages, instanceId } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const quiz = section.quiz
const questions = [...(quiz?.questions ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))

View File

@ -1,12 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useVisitor } from '@/context/VisitorContext'
import { t, tPlain } from '@/lib/i18n'
import type { SectionDTO } from '@/lib/api/types'
import AppBar from '@/components/ui/AppBar'
import ResourceViewer from '@/components/ui/ResourceViewer'
interface Props {
section: SectionDTO
@ -20,7 +20,7 @@ export default function SliderSection({ section, languages }: Props) {
const router = useRouter()
const [index, setIndex] = useState(0)
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const contents = [...(section.slider?.contents ?? [])]
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
@ -45,13 +45,10 @@ export default function SliderSection({ section, languages }: Props) {
<main style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Image zone — ~75% of remaining height */}
<div style={{ flex: 3, minHeight: 0, position: 'relative' }}>
{current?.resource?.url && (
<Image
src={current.resource.url}
{current?.resource && (
<ResourceViewer
resource={current.resource}
alt={tPlain(current.title, language)}
fill
className="object-contain"
sizes="100vw"
/>
)}

View File

@ -38,7 +38,7 @@ export default function VideoSection({ section, slug, configId, languages }: Pro
const { language, setAvailableLanguages } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const source = section.source ?? section.video?.source
const videoType = useMemo(() => detectType(source), [source])

View File

@ -223,7 +223,7 @@ export default function WeatherSection({ section, languages }: Props) {
const router = useRouter()
const [selectedDay, setSelectedDay] = useState(0)
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const weatherData: WeatherData | null = useMemo(() => {
const raw = section.weather?.result

View File

@ -17,7 +17,7 @@ export default function WebSection({ section, slug, configId, languages }: Props
const { language, setAvailableLanguages } = useVisitor()
const router = useRouter()
useEffect(() => { setAvailableLanguages(languages) }, [languages])
useEffect(() => { setAvailableLanguages([]) }, [languages])
const source = section.web?.source ?? section.source
const title = tPlain(section.title, language)

View File

@ -188,6 +188,16 @@ export default function EscapeProgression({ paths, language }: Props) {
open={showEnd}
title="Bravo !"
html={`Tu as terminé le parcours « ${tPlain(activePath.title, language) || 'parcours'} ».`}
icon={
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ background: 'var(--color-primary-light)' }}
>
<svg width="32" height="32" viewBox="0 0 24 24" fill="var(--color-primary)">
<path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94A5.01 5.01 0 0 0 11 15.9V18H9v2h6v-2h-2v-2.1a5.01 5.01 0 0 0 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z" />
</svg>
</div>
}
onClose={() => { setShowEnd(false); setActivePathId(null) }}
primaryAction={{ label: 'Recommencer', onClick: () => { setCompletedSteps(new Set()); setQuizPassedSteps(new Set()); setTimerExpiredSteps(new Set()); setZoneNotifiedSteps(new Set()); setShowEnd(false) } }}
secondaryAction={{ label: 'Retour', onClick: () => { setShowEnd(false); setActivePathId(null) } }}

View File

@ -6,6 +6,7 @@ interface Props {
open: boolean
title?: string
html?: string
icon?: ReactNode
onClose: () => void
primaryAction?: { label: string; onClick: () => void }
secondaryAction?: { label: string; onClick: () => void }
@ -13,7 +14,7 @@ interface Props {
}
export default function MessageDialog({
open, title, html, onClose, primaryAction, secondaryAction, children,
open, title, html, icon, onClose, primaryAction, secondaryAction, children,
}: Props) {
if (!open) return null
return (
@ -27,24 +28,43 @@ export default function MessageDialog({
}}
>
<style>{`@keyframes mim-pop-in { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }`}</style>
{icon && (
<div className="flex justify-center pt-6 pb-1">
{icon}
</div>
)}
{title && (
<div
className="px-5 pt-5 pb-2 text-center text-xl font-bold"
style={{ color: 'var(--color-text)' }}
>
{title}
</div>
)}
{(html || children) && (
<div
className="px-6 pb-5 text-sm text-center [&_p]:m-0 [&_p+p]:mt-2"
style={{ color: 'var(--color-text-muted)' }}
>
{html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children}
</div>
)}
<div
className="px-5 pt-5 pb-3 text-center text-lg font-bold"
style={{ color: 'var(--color-text)' }}
className="flex flex-col gap-2 px-5 pb-5 pt-3"
style={{ borderTop: '1px solid var(--color-border)' }}
>
{title}
</div>
<div
className="px-6 pb-4 text-sm text-center [&_p]:m-0 [&_p+p]:mt-2"
style={{ color: 'var(--color-text)' }}
>
{html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children}
</div>
<div className="flex flex-col gap-2 px-5 pb-5 pt-2">
{primaryAction && (
<button
onClick={primaryAction.onClick}
className="w-full py-3 rounded-2xl font-semibold text-sm"
style={{ background: 'var(--color-primary)', color: 'var(--color-on-primary)' }}
style={{
background: 'var(--color-primary)',
color: 'var(--color-on-primary)',
}}
>
{primaryAction.label}
</button>
@ -54,7 +74,7 @@ export default function MessageDialog({
onClick={secondaryAction.onClick}
className="w-full py-3 rounded-2xl font-semibold text-sm"
style={{
background: 'var(--color-surface)',
background: 'transparent',
color: 'var(--color-text)',
border: '1px solid var(--color-border)',
}}
@ -66,7 +86,10 @@ export default function MessageDialog({
<button
onClick={onClose}
className="w-full py-3 rounded-2xl font-semibold text-sm"
style={{ background: 'var(--color-primary)', color: 'var(--color-on-primary)' }}
style={{
background: 'var(--color-primary)',
color: 'var(--color-on-primary)',
}}
>
OK
</button>

View File

@ -145,7 +145,7 @@ export default function PuzzleGame({ imageUrl, rows, cols, showHint, onWin }: Pr
>
{board && (
<>
{/* Board frame (target outlines) */}
{/* Board frame (always visible) */}
<div
className="absolute"
style={{
@ -153,14 +153,28 @@ export default function PuzzleGame({ imageUrl, rows, cols, showHint, onWin }: Pr
top: `calc(50% - ${board.h / 2}px)`,
width: board.w,
height: board.h,
backgroundImage: showHint ? `url(${imageUrl})` : undefined,
backgroundSize: 'cover',
opacity: showHint ? 0.25 : 1,
border: '1.5px dashed rgba(0,0,0,0.18)',
borderRadius: 8,
background: showHint ? undefined : 'rgba(255,255,255,0.4)',
background: 'rgba(255,255,255,0.4)',
}}
/>
{/* Hint overlay — full image reference when hint is active */}
{showHint && (
<div
className="absolute"
style={{
left: `calc(50% - ${board.w / 2}px)`,
top: `calc(50% - ${board.h / 2}px)`,
width: board.w,
height: board.h,
backgroundImage: `url(${imageUrl})`,
backgroundSize: 'cover',
opacity: 0.55,
borderRadius: 8,
zIndex: 0,
}}
/>
)}
{/* Pieces */}
{pieces.map((p) => {
const pw = board.w / cols

View File

@ -1,5 +1,6 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { useVisitor } from '@/context/VisitorContext'
const FLAG: Record<string, string> = {
@ -7,6 +8,11 @@ const FLAG: Record<string, string> = {
IT: '🇮🇹', ES: '🇪🇸', PL: '🇵🇱', CN: '🇨🇳', AR: '🇸🇦', UK: '🇺🇦',
}
const LABEL: Record<string, string> = {
FR: 'Français', NL: 'Nederlands', EN: 'English', DE: 'Deutsch',
IT: 'Italiano', ES: 'Español', PL: 'Polski', CN: '中文', AR: 'العربية', UK: 'Українська',
}
interface Props {
title?: string
onBack?: () => void
@ -14,6 +20,19 @@ interface Props {
export default function AppBar({ title, onBack }: Props) {
const { language, setLanguage, availableLanguages } = useVisitor()
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])
const showSelector = availableLanguages.length > 1
return (
<header
@ -22,14 +41,13 @@ export default function AppBar({ title, onBack }: Props) {
background: 'linear-gradient(135deg, var(--color-primary), var(--color-secondary))',
minHeight: 56,
color: 'var(--color-on-primary)',
textShadow: '0 1px 3px rgba(0,0,0,0.15)',
}}
>
<div className="flex items-center gap-3 flex-1">
<div className="flex items-center gap-3 flex-1 min-w-0">
{onBack && (
<button
onClick={onBack}
className="p-1 rounded-full hover:opacity-70 transition-opacity"
className="shrink-0 p-1 rounded-full hover:opacity-70 transition-opacity"
aria-label="Retour"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@ -42,25 +60,58 @@ export default function AppBar({ title, onBack }: Props) {
</span>
</div>
{availableLanguages.length > 1 && (
<div className="flex items-center gap-1">
{availableLanguages.map((lang) => {
const active = lang === language
return (
<button
key={lang}
onClick={() => setLanguage(lang)}
className="text-xs font-semibold px-2 py-1 rounded-lg transition-all"
style={{
background: active ? 'rgba(255,255,255,0.25)' : 'transparent',
color: 'var(--color-on-primary)',
opacity: active ? 1 : 0.6,
}}
>
{FLAG[lang] ?? lang}
</button>
)
})}
{showSelector && (
<div ref={ref} className="relative shrink-0 ml-3">
<button
onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1.5 rounded-xl px-3 py-1.5 transition-all"
style={{ background: 'rgba(255,255,255,0.18)', color: 'var(--color-on-primary)' }}
>
<span className="text-lg leading-none">{FLAG[language] ?? language}</span>
<span className="text-xs font-semibold tracking-wide">{language}</span>
<svg
width="14" height="14" viewBox="0 0 24 24" fill="currentColor"
style={{ transition: 'transform 0.2s', transform: open ? 'rotate(180deg)' : 'rotate(0deg)', opacity: 0.8 }}
>
<path d="M7 10l5 5 5-5z"/>
</svg>
</button>
{open && (
<div
className="absolute right-0 mt-1 rounded-2xl overflow-hidden"
style={{
background: 'white',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
minWidth: 160,
zIndex: 100,
}}
>
{availableLanguages.map((lang) => {
const active = lang === language
return (
<button
key={lang}
onClick={() => { setLanguage(lang); setOpen(false) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors"
style={{
background: active ? 'var(--color-primary-light)' : 'transparent',
color: active ? 'var(--color-primary)' : '#333',
fontWeight: active ? 600 : 400,
}}
>
<span className="text-xl leading-none">{FLAG[lang] ?? '🌐'}</span>
<span className="text-sm">{LABEL[lang] ?? lang}</span>
{active && (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="ml-auto" style={{ color: 'var(--color-primary)' }}>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
)}
</button>
)
})}
</div>
)}
</div>
)}
</header>

View File

@ -0,0 +1,89 @@
'use client'
import Image from 'next/image'
import type { ResourceDTO } from '@/lib/api/types'
// Mirrors C# ResourceType enum — backend may serialize as int OR as string name
const IMAGES = new Set([0, 2, 'Image', 'ImageUrl'])
const VIDEOS = new Set([1, 3, 'Video', 'VideoUrl'])
const AUDIOS = new Set([4, 'Audio'])
const VIDEO_URLS = new Set([3, 'VideoUrl'])
function youtubeEmbedUrl(url: string): string | null {
const match = url.match(/(?:v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/)
const id = match?.[1]
return id ? `https://www.youtube.com/embed/${id}?rel=0` : null
}
function vimeoEmbedUrl(url: string): string | null {
const match = url.match(/vimeo\.com\/(?:.*?\/)?(\d+)/)
const id = match?.[1]
return id ? `https://player.vimeo.com/video/${id}` : null
}
interface Props {
resource: ResourceDTO
alt?: string
objectFit?: 'contain' | 'cover'
}
/**
* Renders any ResourceDTO inside a positioned parent (position:relative + defined size).
* Mirrors Flutter's showElementForResource / CachedCustomResource.
*
* Parent must have: position relative, explicit width & height (or fill via flex).
*/
export default function ResourceViewer({ resource, alt = '', objectFit = 'contain' }: Props) {
const { url, type } = resource
if (!url) return null
const t = type as string | number | undefined
// Audio
if (t !== undefined && AUDIOS.has(t as never)) {
return (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<audio controls src={url} style={{ width: '100%', maxWidth: 400 }} />
</div>
)
}
// VideoUrl (YouTube / Vimeo) → iframe; other video URLs → <video>
if (t !== undefined && VIDEO_URLS.has(t as never)) {
const embedUrl = youtubeEmbedUrl(url) ?? vimeoEmbedUrl(url)
if (embedUrl) {
return (
<iframe
src={embedUrl}
style={{ width: '100%', height: '100%', border: 'none' }}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title={alt}
/>
)
}
}
// Video (direct file) or VideoUrl non-embed
if (t !== undefined && VIDEOS.has(t as never)) {
return (
<video
src={url}
controls
playsInline
style={{ width: '100%', height: '100%', objectFit }}
/>
)
}
// Image, ImageUrl, or unknown → render as image (safe default)
return (
<Image
src={url}
alt={alt}
fill
className={`object-${objectFit}`}
sizes="100vw"
/>
)
}

View File

@ -1,5 +1,12 @@
import { getLuminance, parseToRgba } from 'color2k'
// Flutter sends colors as "Color(0xAARRGGBB)" — convert to "#RRGGBB"
function normalizeColor(color: string): string {
const match = color.match(/Color\(0x[0-9a-fA-F]{2}([0-9a-fA-F]{6})\)/)
if (match) return `#${match[1]}`
return color
}
export interface ThemeColors {
'--color-primary': string
'--color-secondary': string
@ -37,7 +44,7 @@ export function resolveColors(
instance: { primaryColor?: string; secondaryColor?: string },
config?: { primaryColor?: string; secondaryColor?: string }
): ThemeColors {
const primary = config?.primaryColor || instance.primaryColor || '#264863'
const secondary = config?.secondaryColor || instance.secondaryColor || '#C2C9D6'
const primary = normalizeColor(config?.primaryColor || instance.primaryColor || '#264863')
const secondary = normalizeColor(config?.secondaryColor || instance.secondaryColor || '#C2C9D6')
return buildTheme(primary, secondary)
}