Merge remote-tracking branch 'gitea/main'
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m13s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 49s
CI / test-python-voice (push) Successful in 38s
CI / test-bqas (push) Successful in 31s
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m13s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 49s
CI / test-python-voice (push) Successful in 38s
CI / test-bqas (push) Successful in 31s
# Conflicts: # pitch-deck/components/slides/MilestonesSlide.tsx # pitch-deck/lib/finanzplan/engine.ts
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// MilestonesSlide data — extracted from MilestonesSlide.tsx
|
||||
// Contains: Milestone/StatItem interfaces, MILESTONES array, STATS array, TODAY_POSITION
|
||||
|
||||
export const TODAY_POSITION = 0.56
|
||||
|
||||
@@ -19,7 +20,8 @@ export interface StatItem { k: { de: string; en: string }; v: string; tint: stri
|
||||
|
||||
export const MILESTONES: Milestone[] = [
|
||||
{
|
||||
id: 'ihk', when: 'Okt. 2025', tick: '10 \u00b7 25',
|
||||
id: 'ihk',
|
||||
when: 'Okt. 2025', tick: '10 \u00b7 25',
|
||||
title: { de: 'Gr\u00fcnderzuschuss & IHK', en: 'Founder Grant & IHK' },
|
||||
short: { de: 'Abstimmung mit Agentur f\u00fcr Arbeit und IHK Konstanz.', en: 'Coordination with Employment Agency and IHK Konstanz.' },
|
||||
body: {
|
||||
@@ -33,7 +35,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#a78bfa', done: true,
|
||||
},
|
||||
{
|
||||
id: 'brand', when: '11. Nov. 2025', tick: '11 \u00b7 25',
|
||||
id: 'brand',
|
||||
when: '11. Nov. 2025', tick: '11 \u00b7 25',
|
||||
title: { de: 'Markenanmeldung & Domains', en: 'Trademark Filing & Domains' },
|
||||
short: { de: 'DPMA-Anmeldung BreakPilot + Domain-Portfolio.', en: 'DPMA filing BreakPilot + domain portfolio.' },
|
||||
body: {
|
||||
@@ -47,7 +50,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#a78bfa', done: true,
|
||||
},
|
||||
{
|
||||
id: 'dev', when: 'Jan. 2026', tick: '01 \u00b7 26',
|
||||
id: 'dev',
|
||||
when: 'Jan. 2026', tick: '01 \u00b7 26',
|
||||
title: { de: 'Plattform-Entwicklung gestartet', en: 'Platform Development Started' },
|
||||
short: { de: '500.000+ Lines of Code, vollst\u00e4ndige Architektur.', en: '500,000+ lines of code, full architecture.' },
|
||||
body: {
|
||||
@@ -61,7 +65,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#c084fc', done: true,
|
||||
},
|
||||
{
|
||||
id: 'dpma', when: '27. M\u00e4r. 2026', tick: '03 \u00b7 26',
|
||||
id: 'dpma',
|
||||
when: '27. M\u00e4r. 2026', tick: '03 \u00b7 26',
|
||||
title: { de: 'Markeneintragung DPMA', en: 'DPMA Trademark Registration' },
|
||||
short: { de: 'BreakPilot offiziell eingetragen.', en: 'BreakPilot officially registered.' },
|
||||
body: {
|
||||
@@ -75,7 +80,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#c084fc', done: true,
|
||||
},
|
||||
{
|
||||
id: 'rag', when: 'Apr. 2026', tick: '04 \u00b7 26',
|
||||
id: 'rag',
|
||||
when: 'Apr. 2026', tick: '04 \u00b7 26',
|
||||
title: { de: 'RAG mit 375+ Dokumenten', en: 'RAG with 375+ Documents' },
|
||||
short: { de: 'EU + DACH Regularien indexiert.', en: 'EU + DACH regulations indexed.' },
|
||||
body: {
|
||||
@@ -89,7 +95,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#c084fc', done: true,
|
||||
},
|
||||
{
|
||||
id: 'euipo', when: '1. Mai 2026', tick: '05 \u00b7 26',
|
||||
id: 'euipo',
|
||||
when: '1. Mai 2026', tick: '05 \u00b7 26',
|
||||
title: { de: 'Markenanmeldung EUIPO', en: 'EUIPO Trademark Filing' },
|
||||
short: { de: 'EU-weiter Markenschutz beantragt.', en: 'EU-wide trademark protection filed.' },
|
||||
body: {
|
||||
@@ -103,7 +110,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#fbbf24', done: false, next: true,
|
||||
},
|
||||
{
|
||||
id: 'gmbh', when: 'Aug. 2026', tick: '08 \u00b7 26',
|
||||
id: 'gmbh',
|
||||
when: 'Aug. 2026', tick: '08 \u00b7 26',
|
||||
title: { de: 'GmbH-Gr\u00fcndung', en: 'GmbH Incorporation' },
|
||||
short: { de: 'Breakpilot COMPLAI GmbH gegr\u00fcndet.', en: 'Breakpilot COMPLAI GmbH incorporated.' },
|
||||
body: {
|
||||
@@ -117,7 +125,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#fbbf24', done: false,
|
||||
},
|
||||
{
|
||||
id: 'customers', when: 'Aug. 2026', tick: '08 \u00b7 26',
|
||||
id: 'customers',
|
||||
when: 'Aug. 2026', tick: '08 \u00b7 26',
|
||||
title: { de: '2 zahlende Kunden', en: '2 Paying Customers' },
|
||||
short: { de: 'Erste Ums\u00e4tze ab Gr\u00fcndung.', en: 'First revenue from incorporation.' },
|
||||
body: {
|
||||
@@ -131,7 +140,8 @@ export const MILESTONES: Milestone[] = [
|
||||
tint: '#fbbf24', done: false,
|
||||
},
|
||||
{
|
||||
id: 'beta', when: 'Q3 2026', tick: 'Q3 \u00b7 26',
|
||||
id: 'beta',
|
||||
when: 'Q3 2026', tick: 'Q3 \u00b7 26',
|
||||
title: { de: '\u00d6ffentliches Beta', en: 'Public Beta' },
|
||||
short: { de: 'Beta-Launch mit ersten zahlenden Kunden.', en: 'Beta launch with first paying customers.' },
|
||||
body: {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { type Milestone, type StatItem, MILESTONES, TODAY_POSITION } from './MilestonesSlide.data'
|
||||
import { type Theme } from './MilestonesSlide.themes'
|
||||
|
||||
const MONO: React.CSSProperties = {
|
||||
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
import { type Milestone, type StatItem, MILESTONES, STATS, TODAY_POSITION } from './MilestonesSlide.data'
|
||||
import { type Theme, MONO } from './MilestonesSlide.themes'
|
||||
|
||||
// ── Star Field ────────────────────────────────────────────────────────────────
|
||||
export function StarField() {
|
||||
@@ -150,7 +145,7 @@ function MilestoneNode({ m, onClick, active, t, de }: {
|
||||
const bgTopA = lit ? m.tint + t.cardTintTopH : m.tint + t.cardTintTop
|
||||
const bgMidA = lit ? m.tint + t.cardTintMidH : m.tint + t.cardTintMid
|
||||
const cardBg = `linear-gradient(180deg, ${bgTopA} 0%, ${bgMidA} 55%, ${t.cardBase}${lit ? t.cardBaseAH : t.cardBaseA})`
|
||||
const badge = m.done ? (de ? 'erledigt' : 'done') : (m.next ? (de ? 'als nächstes' : 'next') : (de ? 'geplant' : 'plan'))
|
||||
const badge = m.done ? (de ? 'erledigt' : 'done') : (m.next ? (de ? 'als n\u00e4chstes' : 'next') : (de ? 'geplant' : 'plan'))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -175,7 +170,7 @@ function MilestoneNode({ m, onClick, active, t, de }: {
|
||||
transition: 'all .25s',
|
||||
transform: lit ? 'scale(1.15)' : 'scale(1)',
|
||||
}}>
|
||||
{m.done ? '✓' : (m.next ? '◉' : '○')}
|
||||
{m.done ? '\u2713' : (m.next ? '\u25c9' : '\u25cb')}
|
||||
</div>
|
||||
|
||||
{/* card */}
|
||||
@@ -224,7 +219,7 @@ function MilestoneNode({ m, onClick, active, t, de }: {
|
||||
opacity: lit ? 1 : 0.55,
|
||||
transform: `translateX(${lit ? 0 : -4}px)`,
|
||||
transition: 'all .25s',
|
||||
}}>{de ? 'Details →' : 'Details →'}</span>
|
||||
}}>{de ? 'Details \u2192' : 'Details \u2192'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -292,7 +287,7 @@ export function DetailModal({ item, onClose, t, de }: {
|
||||
const tint = item.tint
|
||||
const badge = item.done
|
||||
? (de ? 'ABGESCHLOSSEN' : 'COMPLETED')
|
||||
: (item.next ? (de ? 'ALS NÄCHSTES' : 'NEXT UP') : (de ? 'GEPLANT' : 'PLANNED'))
|
||||
: (item.next ? (de ? 'ALS N\u00c4CHSTES' : 'NEXT UP') : (de ? 'GEPLANT' : 'PLANNED'))
|
||||
const badgeColor = item.done ? t.done : tint
|
||||
|
||||
return (
|
||||
@@ -319,7 +314,7 @@ export function DetailModal({ item, onClose, t, de }: {
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: t.key === 'light' ? tint : '#fff', fontSize: 17, fontWeight: 700,
|
||||
boxShadow: `0 0 20px ${tint}66`,
|
||||
}}>{item.done ? '✓' : (item.next ? '◉' : '○')}</div>
|
||||
}}>{item.done ? '\u2713' : (item.next ? '\u25c9' : '\u25cb')}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||
<span style={{
|
||||
@@ -337,7 +332,7 @@ export function DetailModal({ item, onClose, t, de }: {
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: `1px solid ${tint}66`, color: t.fg,
|
||||
width: 32, height: 32, borderRadius: 8, cursor: 'pointer', fontSize: 14,
|
||||
}}>✕</button>
|
||||
}}>{'\u2715'}</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.6, color: t.fgSoft, marginBottom: 16 }}>
|
||||
{de ? item.body.de : item.body.en}
|
||||
@@ -350,7 +345,7 @@ export function DetailModal({ item, onClose, t, de }: {
|
||||
background: t.bulletBg, border: `1px solid ${tint}44`,
|
||||
}}>
|
||||
<span style={{ color: item.done ? t.done : tint, fontSize: 12, marginTop: 1 }}>
|
||||
{item.done ? '✓' : '▸'}
|
||||
{item.done ? '\u2713' : '\u25b8'}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.5, color: t.fgSoft }}>{b}</span>
|
||||
</div>
|
||||
@@ -360,3 +355,87 @@ export function DetailModal({ item, onClose, t, de }: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Inner slide (fixed 1280x600) ─────────────────────────────────────────────
|
||||
export function MilestonesInner({ t, de, sel, setSel }: {
|
||||
t: Theme; de: boolean
|
||||
sel: Milestone | null
|
||||
setSel: (m: Milestone | null) => void
|
||||
}) {
|
||||
const doneCnt = useMemo(() => MILESTONES.filter(m => m.done).length, [])
|
||||
const total = MILESTONES.length
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', width: 1280, height: 600, overflow: 'hidden',
|
||||
background: t.bg, color: t.fg,
|
||||
fontFamily: '"Inter", system-ui, sans-serif', WebkitFontSmoothing: 'antialiased',
|
||||
}}>
|
||||
{/* Ambient glow */}
|
||||
<div style={{
|
||||
position: 'absolute', top: -120, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 800, height: 500, borderRadius: '50%',
|
||||
background: t.ambient, filter: 'blur(50px)', pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
{t.stars ? <StarField /> : <SoftGrid t={t} />}
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 36, right: 52, display: 'flex', alignItems: 'center', gap: 10, zIndex: 3,
|
||||
}}>
|
||||
<div style={{ ...MONO, fontSize: 10, letterSpacing: 2, color: t.fgMuted, textTransform: 'uppercase' as const, fontWeight: 700 }}>
|
||||
{de ? 'Fortschritt' : 'Progress'}
|
||||
</div>
|
||||
<div style={{
|
||||
width: 120, height: 6, background: t.progressTrackBg, borderRadius: 3, overflow: 'hidden',
|
||||
border: `1px solid ${t.progressTrackBorder}`,
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${(doneCnt / total) * 100}%`, height: '100%',
|
||||
background: `linear-gradient(90deg, ${t.done}, ${t.accent})`,
|
||||
boxShadow: `0 0 12px ${t.done}99`,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ ...MONO, fontSize: 11, color: t.fg, fontWeight: 700 }}>
|
||||
<span style={{ color: t.done }}>{doneCnt}</span>
|
||||
<span style={{ color: t.fgWhisper }}> / {total}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tip */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 36, left: 52, ...MONO, fontSize: 10,
|
||||
letterSpacing: 2, color: t.fgGhost, textTransform: 'uppercase' as const, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', gap: 8, zIndex: 3,
|
||||
}}>
|
||||
<span>{de ? 'Tipp:' : 'Tip:'}</span>
|
||||
<span style={{ color: t.accent70 }}>{de ? 'Klick auf einen Meilenstein' : 'Click any milestone'}</span>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div style={{ position: 'relative', marginTop: 68 }}>
|
||||
<Timeline onSelect={setSel} selectedId={sel?.id ?? null} t={t} de={de} />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{
|
||||
position: 'absolute', left: 40, right: 40, bottom: 36,
|
||||
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 14,
|
||||
}}>
|
||||
{STATS.map(s => <StatCard key={s.tint} item={s} t={t} de={de} />)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 14, textAlign: 'center',
|
||||
...MONO, fontSize: 9, letterSpacing: 3, color: t.accent40,
|
||||
textTransform: 'uppercase' as const, fontWeight: 700,
|
||||
}}>
|
||||
{de ? 'Stand heute \u00b7 live-Metriken aus der Plattform' : 'As of today \u00b7 live metrics from the platform'}
|
||||
</div>
|
||||
|
||||
<DetailModal item={sel} onClose={() => setSel(null)} t={t} de={de} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,42 @@
|
||||
// MilestonesSlide themes — extracted from MilestonesSlide.tsx
|
||||
// Contains: THEMES, Theme type, useIsLight hook, MONO style, CSS_KF keyframes
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const MONO: React.CSSProperties = {
|
||||
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
|
||||
export const CSS_KF = `
|
||||
@keyframes msFlow { 0%{stroke-dashoffset:0} 100%{stroke-dashoffset:-18} }
|
||||
@keyframes msFadeIn { from{opacity:0} to{opacity:1} }
|
||||
@keyframes msScaleIn { from{opacity:0;transform:scale(.94)} to{opacity:1;transform:scale(1)} }
|
||||
@keyframes msHeadingDark {
|
||||
0%,100%{text-shadow:0 0 22px rgba(167,139,250,.3)}
|
||||
50% {text-shadow:0 0 40px rgba(167,139,250,.6)}
|
||||
}
|
||||
@keyframes msHeadingLight {
|
||||
0%,100%{text-shadow:0 0 22px rgba(124,58,237,.15)}
|
||||
50% {text-shadow:0 0 36px rgba(124,58,237,.30)}
|
||||
}
|
||||
@keyframes msPulse {
|
||||
0%,100%{r:9;opacity:.4}
|
||||
50% {r:14;opacity:.05}
|
||||
}
|
||||
`
|
||||
|
||||
export function useIsLight() {
|
||||
const [isLight, setIsLight] = useState(false)
|
||||
useEffect(() => {
|
||||
const check = () => setIsLight(document.documentElement.classList.contains('theme-light'))
|
||||
check()
|
||||
const obs = new MutationObserver(check)
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
return () => obs.disconnect()
|
||||
}, [])
|
||||
return isLight
|
||||
}
|
||||
|
||||
export const THEMES = {
|
||||
dark: {
|
||||
@@ -103,4 +141,4 @@ export const THEMES = {
|
||||
},
|
||||
}
|
||||
|
||||
export type Theme = typeof THEMES.dark
|
||||
export type Theme = typeof THEMES.dark | typeof THEMES.light
|
||||
|
||||
@@ -1,137 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
|
||||
import { type Milestone, MILESTONES, STATS } from './MilestonesSlide.data'
|
||||
import { THEMES } from './MilestonesSlide.themes'
|
||||
import { StarField, SoftGrid, Timeline, StatCard, DetailModal } from './MilestonesSlide.parts'
|
||||
import { type Milestone } from './MilestonesSlide.data'
|
||||
import { THEMES, CSS_KF, useIsLight } from './MilestonesSlide.themes'
|
||||
import { MilestonesInner } from './MilestonesSlide.parts'
|
||||
|
||||
interface MilestonesSlideProps { lang: Language }
|
||||
|
||||
const MONO: React.CSSProperties = {
|
||||
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
|
||||
const CSS_KF = `
|
||||
@keyframes msFlow { 0%{stroke-dashoffset:0} 100%{stroke-dashoffset:-18} }
|
||||
@keyframes msFadeIn { from{opacity:0} to{opacity:1} }
|
||||
@keyframes msScaleIn { from{opacity:0;transform:scale(.94)} to{opacity:1;transform:scale(1)} }
|
||||
@keyframes msHeadingDark {
|
||||
0%,100%{text-shadow:0 0 22px rgba(167,139,250,.3)}
|
||||
50% {text-shadow:0 0 40px rgba(167,139,250,.6)}
|
||||
}
|
||||
@keyframes msHeadingLight {
|
||||
0%,100%{text-shadow:0 0 22px rgba(124,58,237,.15)}
|
||||
50% {text-shadow:0 0 36px rgba(124,58,237,.30)}
|
||||
}
|
||||
@keyframes msPulse {
|
||||
0%,100%{r:9;opacity:.4}
|
||||
50% {r:14;opacity:.05}
|
||||
}
|
||||
`
|
||||
|
||||
// ── Light mode hook ───────────────────────────────────────────────────────────
|
||||
function useIsLight() {
|
||||
const [isLight, setIsLight] = useState(false)
|
||||
useEffect(() => {
|
||||
const check = () => setIsLight(document.documentElement.classList.contains('theme-light'))
|
||||
check()
|
||||
const obs = new MutationObserver(check)
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
return () => obs.disconnect()
|
||||
}, [])
|
||||
return isLight
|
||||
}
|
||||
|
||||
// ── Inner slide (fixed 1280×680) ──────────────────────────────────────────────
|
||||
function MilestonesInner({ t, de, sel, setSel }: {
|
||||
t: typeof THEMES.dark; de: boolean
|
||||
sel: Milestone | null
|
||||
setSel: (m: Milestone | null) => void
|
||||
}) {
|
||||
const doneCnt = useMemo(() => MILESTONES.filter(m => m.done).length, [])
|
||||
const total = MILESTONES.length
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', width: 1280, height: 600, overflow: 'hidden',
|
||||
background: t.bg, color: t.fg,
|
||||
fontFamily: '"Inter", system-ui, sans-serif', WebkitFontSmoothing: 'antialiased',
|
||||
}}>
|
||||
{/* Ambient glow */}
|
||||
<div style={{
|
||||
position: 'absolute', top: -120, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 800, height: 500, borderRadius: '50%',
|
||||
background: t.ambient, filter: 'blur(50px)', pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
{t.stars ? <StarField /> : <SoftGrid t={t} />}
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 36, right: 52, display: 'flex', alignItems: 'center', gap: 10, zIndex: 3,
|
||||
}}>
|
||||
<div style={{ ...MONO, fontSize: 10, letterSpacing: 2, color: t.fgMuted, textTransform: 'uppercase' as const, fontWeight: 700 }}>
|
||||
{de ? 'Fortschritt' : 'Progress'}
|
||||
</div>
|
||||
<div style={{
|
||||
width: 120, height: 6, background: t.progressTrackBg, borderRadius: 3, overflow: 'hidden',
|
||||
border: `1px solid ${t.progressTrackBorder}`,
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${(doneCnt / total) * 100}%`, height: '100%',
|
||||
background: `linear-gradient(90deg, ${t.done}, ${t.accent})`,
|
||||
boxShadow: `0 0 12px ${t.done}99`,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ ...MONO, fontSize: 11, color: t.fg, fontWeight: 700 }}>
|
||||
<span style={{ color: t.done }}>{doneCnt}</span>
|
||||
<span style={{ color: t.fgWhisper }}> / {total}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tip */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 36, left: 52, ...MONO, fontSize: 10,
|
||||
letterSpacing: 2, color: t.fgGhost, textTransform: 'uppercase' as const, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', gap: 8, zIndex: 3,
|
||||
}}>
|
||||
<span>{de ? 'Tipp:' : 'Tip:'}</span>
|
||||
<span style={{ color: t.accent70 }}>{de ? 'Klick auf einen Meilenstein' : 'Click any milestone'}</span>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div style={{ position: 'relative', marginTop: 68 }}>
|
||||
<Timeline onSelect={setSel} selectedId={sel?.id ?? null} t={t} de={de} />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{
|
||||
position: 'absolute', left: 40, right: 40, bottom: 36,
|
||||
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 14,
|
||||
}}>
|
||||
{STATS.map(s => <StatCard key={s.tint} item={s} t={t} de={de} />)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 14, textAlign: 'center',
|
||||
...MONO, fontSize: 9, letterSpacing: 3, color: t.accent40,
|
||||
textTransform: 'uppercase' as const, fontWeight: 700,
|
||||
}}>
|
||||
{de ? 'Stand heute · live-Metriken aus der Plattform' : 'As of today · live metrics from the platform'}
|
||||
</div>
|
||||
|
||||
<DetailModal item={sel} onClose={() => setSel(null)} t={t} de={de} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main slide ────────────────────────────────────────────────────────────────
|
||||
const INNER_W = 1280
|
||||
const INNER_H = 600
|
||||
|
||||
|
||||
Reference in New Issue
Block a user