setActiveTrack(track)}
- className="group bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-5 md:p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-emerald-500/5">
-
-
{track.tag}
-
+ const SidebarContent = () => (
+ <>
+ {/* Logo */}
+
+
+
-
{track.title}
-
- {track.author}
- {track.duration}
-
+
Falah
+
Coach Portal
- ))}
-
-
-);
-
-/* ==========================================================================
- MISSION CONTROL
- ========================================================================== */
-
-const MissionControlView = ({ setActiveView, setActiveTrack }) => (
-
-
-
-
-
-
-
- Featured Briefing
-
-
{MOCK_AUDIO_TRACKS[0].title}
-
- Master the critical first 100 days of a private equity backed turnaround. Tactical insights from our top Senior Partners.
-
-
-
setActiveTrack(MOCK_AUDIO_TRACKS[0])}
- className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-semibold py-3 px-6 rounded-lg transition-colors w-max text-sm md:text-base">
-
- Play Briefing ({MOCK_AUDIO_TRACKS[0].duration})
-
-
-
- {[
- { label: 'AI Simulator', desc: 'Practice hostile boardroom scenarios.', view: 'simulator', icon: MessageSquareQuote },
- { label: 'Loop Wiki', desc: 'Review the Falah MECE Guidelines.', view: 'wiki', icon: BookOpen },
- ].map(({ label, desc, view, icon: Icon }) => (
-
setActiveView(view)}
- className="bg-slate-900 border border-slate-800 hover:border-blue-500/40 rounded-2xl p-5 md:p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-blue-500/5 flex-1 flex flex-col justify-between">
-
-
-
-
-
- ))}
-
-
-
-);
-
-/* ==========================================================================
- WIKI VIEW
- ========================================================================== */
-
-const MSLoopWikiView = () => {
- const [activePage, setActivePage] = useState('Falah MECE Guidelines');
- return (
-
-
-
-
-
Directory
-
- {MOCK_WIKI_DIRECTORY.map((dir, idx) => (
-
-
- {dir.folder}
-
-
- {dir.items.map((item) => (
- setActivePage(item)}
- className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors text-left ${activePage === item ? 'bg-blue-500/10 text-blue-400 font-medium' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'}`}>
- {item}
-
- ))}
-
-
- ))}
-
-
-
-
-
-
- loop.microsoft.com/falah/{activePage.toLowerCase().replace(/ /g, '-')}
-
-
-
-
-
{activePage}
-
This document serves as the canonical reference for exactly how our consultants should approach problem structuring in this domain.
-
-
-
Key Objective
-
Drive unambiguous clarity to executive stakeholders within 3 minutes of engagement.
-
-
-
Success Metric
-
Achieve client alignment before the first break in the agenda.
-
-
-
1. Executive Summary
-
The imperative here is not just structural elegance, but actionable intelligence. When framing the issue, prioritise the financial delta and the organisational capacity required to capture it.
-
- Identify the core constraint (Capital, Talent, or Regulatory).
- Map the adjacent capabilities required.
- Draft the 100-day execution roadmap.
-
-
-
-
-
-
- );
-};
-
-/* ==========================================================================
- MAIN APP — Audio state managed with refs to avoid constant re-renders
- ========================================================================== */
-
-export default function App() {
- // ── React state — only what MUST cause re-renders ────────────────────────
- const [activeView, setActiveView] = useState('missionControl');
- const [activeTrack, setActiveTrack] = useState(null); // track object | null
- const [trackIndex, setTrackIndex] = useState(0);
- const [isPlaying, setIsPlaying] = useState(false);
- const [sidebarOpen, setSidebarOpen] = useState(false);
-
- // ── Refs — updated without triggering re-renders ─────────────────────────
- const utteranceRef = useRef(null);
- const ttsIntervalRef = useRef(null);
- const ttsStartTimeRef = useRef(0);
- const ttsElapsedRef = useRef(0);
- const durationRef = useRef(0); // estimated total duration (seconds)
- const activeTrackRef = useRef(null); // mirrors activeTrack
-
- // DOM refs for live progress updates (direct DOM writes — no re-render)
- const desktopProgressRef = useRef(null); // sidebar progress bar fill
- const mobileProgressRef = useRef(null); // mobile strip progress bar fill
- const desktopTimestampRef = useRef(null); // "00:25 / 14:20"
- const mobileTimestampRef = useRef(null); // mobile "Now Playing · 00:25"
-
- // ── Direct DOM update helper ─────────────────────────────────────────────
- const updateProgressDOM = useCallback((elapsed) => {
- const dur = durationRef.current;
- const pct = dur > 0 ? Math.min((elapsed / dur) * 100, 100) : 0;
- const tStr = formatTime(elapsed);
- const track = activeTrackRef.current;
-
- if (desktopProgressRef.current) desktopProgressRef.current.style.width = `${pct}%`;
- if (mobileProgressRef.current) mobileProgressRef.current.style.width = `${pct}%`;
- if (desktopTimestampRef.current) desktopTimestampRef.current.textContent = `${tStr} / ${track?.duration ?? ''}`;
- if (mobileTimestampRef.current) mobileTimestampRef.current.textContent = tStr;
- }, []);
-
- // ── TTS helpers ──────────────────────────────────────────────────────────
- const stopTTS = useCallback(() => {
- window.speechSynthesis.cancel();
- clearInterval(ttsIntervalRef.current);
- utteranceRef.current = null;
- }, []);
-
- const startTTS = useCallback((track, fromSeconds = 0) => {
- if (!track?.ttsScript) return;
- stopTTS();
-
- const estimatedDuration = track.ttsScript.length / 12.5;
- durationRef.current = estimatedDuration;
- activeTrackRef.current = track;
-
- const charOffset = Math.floor((fromSeconds / estimatedDuration) * track.ttsScript.length);
- const scriptSlice = track.ttsScript.slice(charOffset);
-
- const utt = new SpeechSynthesisUtterance(scriptSlice);
- utt.rate = 0.92; utt.pitch = 1.0; utt.volume = 1.0;
- const voices = window.speechSynthesis.getVoices();
- const preferred = voices.find((v) => v.lang.startsWith('en') && v.name.toLowerCase().includes('google'))
- || voices.find((v) => v.lang.startsWith('en')) || null;
- if (preferred) utt.voice = preferred;
-
- ttsStartTimeRef.current = Date.now();
- ttsElapsedRef.current = fromSeconds;
-
- utt.onend = () => { setIsPlaying(false); ttsElapsedRef.current = 0; clearInterval(ttsIntervalRef.current); updateProgressDOM(0); };
- utt.onerror = () => { setIsPlaying(false); clearInterval(ttsIntervalRef.current); };
-
- utteranceRef.current = utt;
- window.speechSynthesis.speak(utt);
- setIsPlaying(true);
-
- // Interval only mutates DOM — NO setState, NO re-render
- ttsIntervalRef.current = setInterval(() => {
- const elapsed = ttsElapsedRef.current + (Date.now() - ttsStartTimeRef.current) / 1000;
- updateProgressDOM(Math.min(elapsed, estimatedDuration));
- }, 250);
- }, [stopTTS, updateProgressDOM]);
-
- // ── Load a track ─────────────────────────────────────────────────────────
- const loadTrack = useCallback((track) => {
- const idx = MOCK_AUDIO_TRACKS.findIndex((t) => t.id === track.id);
- stopTTS();
- setActiveTrack(track);
- setTrackIndex(idx >= 0 ? idx : 0);
- setIsPlaying(false);
- ttsElapsedRef.current = 0;
- durationRef.current = 0;
- updateProgressDOM(0);
- setTimeout(() => startTTS(track, 0), 200);
- }, [stopTTS, startTTS, updateProgressDOM]);
-
- // ── Play / Pause ──────────────────────────────────────────────────────────
- const togglePlay = useCallback(() => {
- if (!activeTrackRef.current) return;
- if (isPlaying) {
- ttsElapsedRef.current = ttsElapsedRef.current + (Date.now() - ttsStartTimeRef.current) / 1000;
- stopTTS();
- setIsPlaying(false);
- } else {
- startTTS(activeTrackRef.current, ttsElapsedRef.current);
- }
- }, [isPlaying, stopTTS, startTTS]);
-
- // ── Skip ─────────────────────────────────────────────────────────────────
- const skipNext = useCallback(() => {
- const next = (trackIndex + 1) % MOCK_AUDIO_TRACKS.length;
- loadTrack(MOCK_AUDIO_TRACKS[next]);
- }, [trackIndex, loadTrack]);
-
- const skipPrev = useCallback(() => {
- const prev = (trackIndex - 1 + MOCK_AUDIO_TRACKS.length) % MOCK_AUDIO_TRACKS.length;
- loadTrack(MOCK_AUDIO_TRACKS[prev]);
- }, [trackIndex, loadTrack]);
-
- // ── Seek ──────────────────────────────────────────────────────────────────
- const seek = useCallback((e) => {
- const dur = durationRef.current;
- if (!activeTrackRef.current || !dur) return;
- const rect = e.currentTarget.getBoundingClientRect();
- const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
- const target = frac * dur;
- ttsElapsedRef.current = target;
- updateProgressDOM(target);
- if (isPlaying) { stopTTS(); setTimeout(() => startTTS(activeTrackRef.current, target), 100); }
- }, [isPlaying, stopTTS, startTTS, updateProgressDOM]);
-
- // Cleanup
- useEffect(() => () => { stopTTS(); }, [stopTTS]);
-
- const handleNavClick = (id) => { setActiveView(id); setSidebarOpen(false); };
-
- const navItems = [
- { id: 'missionControl', label: 'Mission Control', icon: LayoutDashboard },
- { id: 'simulator', label: 'AI Co-Pilot', icon: MessageSquareQuote },
- { id: 'audio', label: 'Audio Library', icon: Headphones },
- { id: 'wiki', label: 'Loop Wiki', icon: BookOpen },
- ];
-
- // ── Render ────────────────────────────────────────────────────────────────
- return (
-
-
- {/* Mobile backdrop */}
- {sidebarOpen && (
-
setSidebarOpen(false)} />
- )}
-
- {/* Sidebar */}
-
-
-
-
setSidebarOpen(false)} className="md:hidden text-slate-500 hover:text-white p-1 mt-1" aria-label="Close menu">
-
-
-
-
-
- Platform
- {navItems.map(({ id, label, icon: Icon }) => {
- const isActive = activeView === id;
- return (
- handleNavClick(id)}
- className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-all duration-200 group ${isActive ? 'bg-blue-600 shadow-md shadow-blue-500/20' : 'hover:bg-slate-800 text-slate-400 hover:text-slate-100'}`}>
-
-
- {label}
-
- {isActive && }
-
- );
- })}
-
-
- {/* Sidebar audio player — inlined, refs for live progress (no re-render) */}
-
- {activeTrack ? (
-
-
-
- Now Playing
-
- {/* timestamp updated directly via ref — no re-render */}
- 00:00 / {activeTrack.duration}
-
-
{activeTrack.title}
-
{activeTrack.author}
- {/* Seekable progress bar — fill updated via ref */}
-
-
-
-
- {isPlaying ? : }
-
-
-
-
- ) : (
-
-
-
Select an audio brief to begin
-
- )}
-
-
-
- {/* Main content column */}
-
-
- {/* Mobile top bar */}
-
- setSidebarOpen(true)} aria-label="Open menu" className="text-slate-400 hover:text-white p-1">
-
-
-
-
-
FALAH
-
Coach Portal
-
- {activeTrack ? (
-
- {isPlaying ? : }
-
- ) :
}
-
-
- {/* Scrollable main */}
-
-
-
- {activeView === 'missionControl' &&
}
- {activeView === 'simulator' &&
}
- {activeView === 'audio' &&
}
- {activeView === 'wiki' &&
}
-
-
-
- {/* Mobile mini audio strip (only shown when track active) */}
- {activeTrack && (
-
- {/* Mobile progress bar fill updated via ref */}
-
-
-
-
{activeTrack.title}
-
- {isPlaying ? 'Now Playing' : 'Paused'} · 00:00
-
-
-
-
-
- {isPlaying ? : }
-
-
-
-
+ {engagement && (
+
+
Active Engagement
+
{engagement.client}
+
{engagement.name}
)}
+
+
+ {/* Nav items */}
+
+ {visibleNav.map(({ id, label, icon: Icon }) => {
+ const isActive = nav.view === id || (id === 'portfolio' && nav.view === 'team');
+ return (
+ navigate(id)}
+ className={`w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
+ isActive
+ ? 'bg-navy-50 text-navy-700 border-l-2 border-navy-700'
+ : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 border-l-2 border-transparent'
+ }`}
+ >
+
+ {label}
+
+ );
+ })}
+
+
+ {/* Persona switcher */}
+
+
+
setPersonaOpen(p => !p)}
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-gray-50 transition-colors"
+ >
+
+ {persona.initials}
+
+
+
{persona.name}
+
{persona.role}
+
+
+
+
+ {personaOpen && (
+
+
Switch Persona
+ {PERSONAS.map(p => (
+
{ setPersonaId(p.id); navigate('dashboard'); setPersonaOpen(false); }}
+ className={`w-full flex items-center gap-2.5 px-3 py-2 text-left hover:bg-gray-50 transition-colors ${personaId === p.id ? 'bg-navy-50' : ''}`}
+ >
+
+ {p.initials}
+
+
+
+ ))}
+
+ )}
+
+
+ >
+ );
+
+ return (
+
+ {/* ── Desktop sidebar ── */}
+
+
+ {/* ── Mobile sidebar backdrop ── */}
+ {sidebarOpen && (
+
setSidebarOpen(false)} />
+ )}
+
+ {/* ── Mobile sidebar ── */}
+
+
+ {/* ── Main content ── */}
+
+ {/* Mobile top bar */}
+
+ setSidebarOpen(true)} className="p-1 text-gray-500">
+
+
+ Falah Coach Portal
+
+
+ {/* View content */}
+
+ {renderView()}
+
{/* Mobile bottom nav */}
-
- {navItems.map(({ id, label, icon: Icon }) => {
- const isActive = activeView === id;
+
+ {visibleNav.slice(0, 4).map(({ id, label, icon: Icon }) => {
+ const isActive = nav.view === id || (id === 'portfolio' && nav.view === 'team');
return (
- handleNavClick(id)} aria-label={label}
- className={`flex flex-col items-center gap-1 px-3 py-1.5 rounded-lg transition-colors ${isActive ? 'text-blue-400' : 'text-slate-500 hover:text-slate-300'}`}>
-
- {label.split(' ')[0]}
+ navigate(id)}
+ className={`flex-1 flex flex-col items-center gap-1 py-2 text-[10px] font-medium transition-colors ${isActive ? 'text-navy-700' : 'text-gray-400'}`}>
+
+ {label.split(' ')[0]}
);
})}
diff --git a/src/data/mock.js b/src/data/mock.js
new file mode 100644
index 0000000..3bc0ff0
--- /dev/null
+++ b/src/data/mock.js
@@ -0,0 +1,397 @@
+// ============================================================
+// FALAH COACH PORTAL — Mock Data
+// All personas, engagements, teams, sessions, actions, workshops
+// ============================================================
+
+export const PERSONAS = [
+ {
+ id: 'marcus',
+ name: 'Marcus Chen',
+ role: 'Senior Agile Coach',
+ initials: 'MC',
+ activeEngagement: 'meridian',
+ defaultView: 'dashboard',
+ },
+ {
+ id: 'priya',
+ name: 'Priya Sharma',
+ role: 'Principal DT Coach',
+ initials: 'PS',
+ activeEngagement: 'bankfirst',
+ defaultView: 'dashboard',
+ },
+ {
+ id: 'david',
+ name: 'David Walsh',
+ role: 'Enterprise Coach · Partner',
+ initials: 'DW',
+ activeEngagement: null,
+ defaultView: 'reports',
+ },
+];
+
+export const ENGAGEMENTS = [
+ {
+ id: 'meridian',
+ client: 'Meridian Telco',
+ name: 'SAFe Transformation',
+ phase: 'Scale',
+ startDate: '2025-02-01',
+ endDate: '2026-07-31',
+ industry: 'Telecommunications',
+ coaches: ['marcus', 'david'],
+ teamCount: 14,
+ description: 'Enterprise-wide SAFe 6.0 adoption across 3 Business Units. 14 product teams, ~180 practitioners.',
+ },
+ {
+ id: 'bankfirst',
+ client: 'BankFirst Group',
+ name: 'Digital Transformation Programme',
+ phase: 'Vision & Alignment',
+ startDate: '2025-10-01',
+ endDate: '2026-09-30',
+ industry: 'Financial Services',
+ coaches: ['priya', 'david'],
+ teamCount: 6,
+ description: '12-month executive-led digital transformation. Aligning 6 C-suite stakeholders on a 3-year digital roadmap.',
+ },
+ {
+ id: 'nexus',
+ client: 'Nexus Energy',
+ name: 'Agile at Scale',
+ phase: 'Foundation',
+ startDate: '2026-03-01',
+ endDate: '2027-02-28',
+ industry: 'Energy & Utilities',
+ coaches: ['david'],
+ teamCount: 8,
+ description: 'Lean-Agile mindset and practice adoption across the IT division. 8 teams, 2 ARTs planned.',
+ },
+];
+
+// ── TEAMS (14 for Meridian) ──────────────────────────────────
+
+const MATURITY_LEVELS = ['Foundation', 'Developing', 'Performing', 'Leading'];
+
+export const TEAMS = [
+ // BU: Digital Products (5 teams)
+ {
+ id: 'tf', name: 'Team Falcon', bu: 'Digital Products', members: 9, assignedCoach: 'marcus',
+ maturityLevel: 'Developing', maturityScore: 2.4, lastSession: '2026-08-15',
+ openActions: 5, status: 'at-risk',
+ maturityHistory: [
+ { month: 'Feb', score: 1.2 }, { month: 'Apr', score: 1.8 },
+ { month: 'Jun', score: 2.1 }, { month: 'Aug', score: 2.4 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'th', name: 'Team Horizon', bu: 'Digital Products', members: 7, assignedCoach: 'marcus',
+ maturityLevel: 'Performing', maturityScore: 3.2, lastSession: '2026-08-20',
+ openActions: 2, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 1.8 }, { month: 'Apr', score: 2.3 },
+ { month: 'Jun', score: 2.9 }, { month: 'Aug', score: 3.2 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tn', name: 'Team Nova', bu: 'Digital Products', members: 11, assignedCoach: 'marcus',
+ maturityLevel: 'Developing', maturityScore: 2.0, lastSession: '2026-08-10',
+ openActions: 8, status: 'blocked',
+ maturityHistory: [
+ { month: 'Feb', score: 1.0 }, { month: 'Apr', score: 1.5 },
+ { month: 'Jun', score: 1.8 }, { month: 'Aug', score: 2.0 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tp', name: 'Team Phoenix', bu: 'Digital Products', members: 8, assignedCoach: 'marcus',
+ maturityLevel: 'Performing', maturityScore: 3.0, lastSession: '2026-08-19',
+ openActions: 1, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 2.0 }, { month: 'Apr', score: 2.4 },
+ { month: 'Jun', score: 2.8 }, { month: 'Aug', score: 3.0 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'ta', name: 'Team Atlas', bu: 'Digital Products', members: 6, assignedCoach: 'marcus',
+ maturityLevel: 'Leading', maturityScore: 3.8, lastSession: '2026-08-21',
+ openActions: 0, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 2.5 }, { month: 'Apr', score: 3.0 },
+ { month: 'Jun', score: 3.5 }, { month: 'Aug', score: 3.8 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ // BU: Network Operations (5 teams)
+ {
+ id: 'tv', name: 'Team Vortex', bu: 'Network Operations', members: 10, assignedCoach: 'marcus',
+ maturityLevel: 'Foundation', maturityScore: 1.3, lastSession: '2026-07-30',
+ openActions: 9, status: 'blocked',
+ maturityHistory: [
+ { month: 'Feb', score: 1.0 }, { month: 'Apr', score: 1.1 },
+ { month: 'Jun', score: 1.2 }, { month: 'Aug', score: 1.3 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'to', name: 'Team Orbit', bu: 'Network Operations', members: 8, assignedCoach: 'marcus',
+ maturityLevel: 'Developing', maturityScore: 2.2, lastSession: '2026-08-18',
+ openActions: 4, status: 'at-risk',
+ maturityHistory: [
+ { month: 'Feb', score: 1.3 }, { month: 'Apr', score: 1.7 },
+ { month: 'Jun', score: 2.0 }, { month: 'Aug', score: 2.2 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tq', name: 'Team Quasar', bu: 'Network Operations', members: 9, assignedCoach: 'marcus',
+ maturityLevel: 'Developing', maturityScore: 2.6, lastSession: '2026-08-20',
+ openActions: 3, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 1.5 }, { month: 'Apr', score: 2.0 },
+ { month: 'Jun', score: 2.3 }, { month: 'Aug', score: 2.6 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'ts', name: 'Team Solstice', bu: 'Network Operations', members: 7, assignedCoach: 'marcus',
+ maturityLevel: 'Performing', maturityScore: 3.1, lastSession: '2026-08-14',
+ openActions: 2, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 1.9 }, { month: 'Apr', score: 2.4 },
+ { month: 'Jun', score: 2.8 }, { month: 'Aug', score: 3.1 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'te', name: 'Team Echo', bu: 'Network Operations', members: 11, assignedCoach: 'marcus',
+ maturityLevel: 'Foundation', maturityScore: 1.6, lastSession: '2026-08-05',
+ openActions: 7, status: 'at-risk',
+ maturityHistory: [
+ { month: 'Feb', score: 1.0 }, { month: 'Apr', score: 1.2 },
+ { month: 'Jun', score: 1.4 }, { month: 'Aug', score: 1.6 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ // BU: Customer Experience (4 teams)
+ {
+ id: 'tpr', name: 'Team Prism', bu: 'Customer Experience', members: 8, assignedCoach: 'marcus',
+ maturityLevel: 'Performing', maturityScore: 3.4, lastSession: '2026-08-22',
+ openActions: 1, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 2.2 }, { month: 'Apr', score: 2.8 },
+ { month: 'Jun', score: 3.1 }, { month: 'Aug', score: 3.4 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tze', name: 'Team Zenith', bu: 'Customer Experience', members: 6, assignedCoach: 'marcus',
+ maturityLevel: 'Leading', maturityScore: 3.9, lastSession: '2026-08-21',
+ openActions: 0, status: 'healthy',
+ maturityHistory: [
+ { month: 'Feb', score: 2.8 }, { month: 'Apr', score: 3.2 },
+ { month: 'Jun', score: 3.6 }, { month: 'Aug', score: 3.9 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tti', name: 'Team Titan', bu: 'Customer Experience', members: 9, assignedCoach: 'marcus',
+ maturityLevel: 'Developing', maturityScore: 2.3, lastSession: '2026-08-16',
+ openActions: 4, status: 'at-risk',
+ maturityHistory: [
+ { month: 'Feb', score: 1.4 }, { month: 'Apr', score: 1.8 },
+ { month: 'Jun', score: 2.0 }, { month: 'Aug', score: 2.3 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+ {
+ id: 'tpu', name: 'Team Pulse', bu: 'Customer Experience', members: 7, assignedCoach: 'marcus',
+ maturityLevel: 'Foundation', maturityScore: 1.5, lastSession: '2026-08-08',
+ openActions: 6, status: 'at-risk',
+ maturityHistory: [
+ { month: 'Feb', score: 1.0 }, { month: 'Apr', score: 1.1 },
+ { month: 'Jun', score: 1.3 }, { month: 'Aug', score: 1.5 },
+ ],
+ sprint: 'PI 4 / Sprint 3',
+ },
+];
+
+// ── SESSIONS ────────────────────────────────────────────────
+
+export const SESSIONS = [
+ {
+ id: 's1', teamId: 'tf', date: '2026-08-15',
+ coach: 'Marcus Chen', type: 'Sprint Retrospective Coaching', duration: 90,
+ aiSummary: 'Team Falcon continues to struggle with PO empowerment — the Product Owner lacks authority to make scope decisions independently. Three overdue actions from the previous session remain open, suggesting follow-through is a systemic issue. The team\'s velocity has stabilised but innovation capacity is limited by persistent tech debt. Recommended next intervention: a focused PO empowerment workshop with Meridian leadership.',
+ tags: [
+ { type: 'blocker', text: 'PO lacks authority to make scope decisions without VP approval' },
+ { type: 'action', text: 'Facilitate PO empowerment workshop with Digital Products VP', owner: 'Marcus', due: '2026-08-29', status: 'open' },
+ { type: 'action', text: 'Team to document top 5 tech debt items and present to architect', owner: 'Team Lead', due: '2026-08-22', status: 'overdue' },
+ { type: 'decision', text: 'Move to 3-week sprints to accommodate dependency management' },
+ { type: 'question', text: 'Is the tech debt backlog visible to the ART and prioritised at PI level?' },
+ ],
+ },
+ {
+ id: 's2', teamId: 'tf', date: '2026-08-01',
+ coach: 'Marcus Chen', type: 'Team Health Check', duration: 60,
+ aiSummary: 'Health check revealed low psychological safety scores — team members hesitant to raise impediments in open forums. Retrospective format is too structured; recommend switching to a more freeform approach to surface real issues. PO absence from daily standups is noted as a key blocker.',
+ tags: [
+ { type: 'blocker', text: 'PO frequently absent from daily standup — team lacks direction' },
+ { type: 'action', text: 'Schedule 1:1 with PO to understand workload and attendance constraints', owner: 'Marcus', due: '2026-08-08', status: 'done' },
+ { type: 'decision', text: 'Switch to "4Ls" retro format next sprint' },
+ ],
+ },
+ {
+ id: 's3', teamId: 'tn', date: '2026-08-10',
+ coach: 'Marcus Chen', type: 'Impediment Removal Workshop', duration: 120,
+ aiSummary: 'Team Nova\'s primary blocker is an architectural dependency on a shared platform team that has a 6-week backlog. This is causing significant flow disruption. The team has 8 open actions, many inherited from previous coaches. Recommend a full action backlog review and ruthless deprioritisation.',
+ tags: [
+ { type: 'blocker', text: 'Platform team dependency — 6-week backlog causing complete flow blockage' },
+ { type: 'tension', text: 'Engineering Manager wants to split team; SM disagrees' },
+ { type: 'action', text: 'Escalate platform dependency to ART level at next System Demo', owner: 'Marcus', due: '2026-08-24', status: 'open' },
+ ],
+ },
+];
+
+// ── ACTIONS ─────────────────────────────────────────────────
+
+export const ACTIONS = [
+ { id: 'a1', teamId: 'tf', sessionId: 's1', text: 'Facilitate PO empowerment workshop with Digital Products VP', owner: 'Marcus', dueDate: '2026-08-29', status: 'open' },
+ { id: 'a2', teamId: 'tf', sessionId: 's1', text: 'Document top 5 tech debt items and present to architect', owner: 'Team Lead', dueDate: '2026-08-22', status: 'overdue' },
+ { id: 'a3', teamId: 'tf', sessionId: 's1', text: 'Review Definition of Done with entire team', owner: 'Scrum Master', dueDate: '2026-08-25', status: 'open' },
+ { id: 'a4', teamId: 'tf', sessionId: 's1', text: 'Present velocity trend to Programme Manager', owner: 'Marcus', dueDate: '2026-09-01', status: 'open' },
+ { id: 'a5', teamId: 'tf', sessionId: 's2', text: 'Schedule 1:1 with PO on workload and attendance', owner: 'Marcus', dueDate: '2026-08-08', status: 'done' },
+ { id: 'a6', teamId: 'tn', sessionId: 's3', text: 'Escalate platform dependency to ART level at System Demo', owner: 'Marcus', dueDate: '2026-08-24', status: 'open' },
+ { id: 'a7', teamId: 'tn', sessionId: 's3', text: 'Deprioritise and close 4 inherited actions over 60 days old', owner: 'Scrum Master', dueDate: '2026-08-17', status: 'overdue' },
+ { id: 'a8', teamId: 'tv', sessionId: null, text: 'Run Agile basics workshop — team has no Scrum experience', owner: 'Marcus', dueDate: '2026-08-15', status: 'overdue' },
+];
+
+// ── WORKSHOPS (Priya / BankFirst) ───────────────────────────
+
+export const WORKSHOPS = [
+ {
+ id: 'w1',
+ engagementId: 'bankfirst',
+ name: 'Digital Vision Alignment — Session 2',
+ date: '2026-08-20',
+ duration: 120,
+ facilitator: 'Priya Sharma',
+ stakeholders: ['CDO', 'CFO', 'CHRO', 'CTO', 'Head of Digital', 'Group Strategy Director'],
+ objective: 'Align executive team on 3-year digital investment priorities and resolve conflicts between cost-reduction and revenue-growth agendas.',
+ phase: 'Vision & Alignment',
+ status: 'completed',
+ aiSummary: 'Strong consensus emerged around customer experience as the primary digital investment thesis. Key tension: CFO prioritises cost-out (£40M target) while CDO prioritises revenue growth through new digital channels. This tension is unresolved and must be mediated before the investment committee meeting. Three decisions were made; five open questions remain.',
+ captures: [
+ { type: 'decision', text: 'Digital investment thesis: Customer Experience as primary driver', stakeholder: 'CDO', timestamp: '09:45' },
+ { type: 'decision', text: 'Core banking modernisation deferred to Year 2', stakeholder: 'CTO', timestamp: '10:20' },
+ { type: 'decision', text: 'Digital talent strategy to be led by CHRO with CDO as sponsor', stakeholder: 'Group', timestamp: '11:10' },
+ { type: 'tension', text: 'CFO: "£40M cost-out target conflicts with any meaningful digital investment in Year 1"', stakeholder: 'CFO', timestamp: '10:50' },
+ { type: 'tension', text: 'CDO and CTO misaligned on build vs. buy for new channel platform', stakeholder: 'CDO/CTO', timestamp: '11:30' },
+ { type: 'question', text: 'What is the ROI model for the digital investment? Who owns it?', stakeholder: 'CFO', timestamp: '10:55' },
+ { type: 'question', text: 'How do we measure "customer experience improvement"?', stakeholder: 'CHRO', timestamp: '09:52' },
+ { type: 'next-step', text: 'Priya to facilitate CFO/CDO bilateral before next group session', owner: 'Priya', due: '2026-08-27' },
+ { type: 'next-step', text: 'CTO to present build vs. buy analysis for channel platform', owner: 'CTO', due: '2026-09-03' },
+ ],
+ },
+ {
+ id: 'w2',
+ engagementId: 'bankfirst',
+ name: 'Organisational Readiness Assessment',
+ date: '2026-08-06',
+ duration: 90,
+ facilitator: 'Priya Sharma',
+ stakeholders: ['CHRO', 'Head of Digital', 'Head of IT', 'Group Strategy Director'],
+ objective: 'Assess current organisational capability and identify the top 3 change management risks for the transformation programme.',
+ phase: 'Vision & Alignment',
+ status: 'completed',
+ aiSummary: 'Readiness assessment revealed significant talent gaps in digital skills and change management capacity. Three critical risk areas identified: (1) leadership alignment deficit, (2) technology debt limiting transformation speed, (3) culture of risk-aversion in the retail banking division.',
+ captures: [
+ { type: 'decision', text: 'Change management capability to be built internally, not outsourced', stakeholder: 'CHRO', timestamp: '09:30' },
+ { type: 'question', text: 'Do we have the internal digital talent to self-sustain after the programme?', stakeholder: 'Head of IT', timestamp: '10:15' },
+ { type: 'tension', text: 'CHRO believes culture change takes 3-5 years; Head of Digital expects results in 12 months', stakeholder: 'CHRO/HoD', timestamp: '10:45' },
+ ],
+ },
+];
+
+// ── PLAYBOOKS ────────────────────────────────────────────────
+
+export const PLAYBOOKS = [
+ {
+ id: 'pb1', category: 'Agile', tag: 'SAFe',
+ title: 'PI Planning Facilitation Guide',
+ description: 'End-to-end facilitation guide for Programme Increment planning events. Covers pre-PI, day 1 briefing, team breakouts, draft plan review, and confidence vote.',
+ duration: '2 days', level: 'Advanced', lastUpdated: '2026-06-15',
+ },
+ {
+ id: 'pb2', category: 'Agile', tag: 'Retrospective',
+ title: 'Team Health Check — Spotify Model',
+ description: 'Structured team health survey and facilitation guide based on the Spotify Squad Health Check model, adapted for SAFe contexts.',
+ duration: '60 min', level: 'Foundation', lastUpdated: '2026-05-20',
+ },
+ {
+ id: 'pb3', category: 'Agile', tag: 'Impediment',
+ title: 'Impediment Removal Workshop',
+ description: 'Structured workshop to surface, categorise, and escalate team-level impediments. Includes ROAM technique for PI planning impediments.',
+ duration: '90 min', level: 'Intermediate', lastUpdated: '2026-07-01',
+ },
+ {
+ id: 'pb4', category: 'Digital Transformation', tag: 'Readiness',
+ title: 'Organisational Readiness Assessment',
+ description: 'Diagnostic tool for assessing an organisation\'s readiness for digital transformation across 5 dimensions: Leadership, Culture, Technology, Data, and Talent.',
+ duration: '90 min', level: 'Intermediate', lastUpdated: '2026-08-01',
+ },
+ {
+ id: 'pb5', category: 'Digital Transformation', tag: 'Alignment',
+ title: 'Executive Vision Alignment Workshop',
+ description: 'Facilitation guide for aligning C-suite stakeholders on a shared digital vision. Includes pre-work templates, facilitation scripts, and decision-capture tools.',
+ duration: '3 hours', level: 'Advanced', lastUpdated: '2026-07-15',
+ },
+ {
+ id: 'pb6', category: 'Facilitation', tag: 'Diverge',
+ title: 'Lean Coffee — Open Forum',
+ description: 'Agenda-less meeting format where participants self-organise discussion topics by dot-voting. Ideal for surfacing hidden impediments and building psychological safety.',
+ duration: '45–60 min', level: 'Foundation', lastUpdated: '2026-04-10',
+ },
+ {
+ id: 'pb7', category: 'Facilitation', tag: 'Converge',
+ title: 'Dot Voting — Priority Alignment',
+ description: 'Simple, fast technique for reaching group consensus on priorities. Works for backlog prioritisation, impediment ranking, and investment decisions.',
+ duration: '20–30 min', level: 'Foundation', lastUpdated: '2026-03-05',
+ },
+ {
+ id: 'pb8', category: 'Enterprise', tag: 'Reporting',
+ title: 'Coaching Impact Report Template',
+ description: 'Structured template for reporting coaching outcomes to executive sponsors. Includes maturity trend charts, milestone summaries, and recommended next steps.',
+ duration: 'Template', level: 'All Levels', lastUpdated: '2026-08-10',
+ },
+];
+
+// ── PORTFOLIO SUMMARY (David) ────────────────────────────────
+
+export const PORTFOLIO_SUMMARY = {
+ totalEngagements: 3,
+ totalTeams: 28,
+ totalCoaches: 4,
+ overallHealth: 'amber',
+ engagementHealth: {
+ meridian: { health: 'amber', teamsHealthy: 7, teamsAtRisk: 5, teamsBlocked: 2, avgMaturity: 2.7 },
+ bankfirst: { health: 'amber', teamsHealthy: 4, teamsAtRisk: 2, teamsBlocked: 0, avgMaturity: null },
+ nexus: { health: 'green', teamsHealthy: 6, teamsAtRisk: 2, teamsBlocked: 0, avgMaturity: 1.4 },
+ },
+ monthlyInsights: [
+ 'Team Vortex and Team Nova remain blocked for 3+ weeks — systemic platform dependency needs ART-level escalation.',
+ 'BankFirst CFO/CDO misalignment is the highest-risk item across the entire portfolio. Bilateral mediation scheduled.',
+ 'Teams Zenith and Atlas have reached Leading maturity — consider case study documentation for Falah IP.',
+ 'Nexus engagement is 2 weeks behind onboarding schedule due to IT infrastructure delays.',
+ ],
+};
+
+// ── HELPER: Today schedule for Marcus ───────────────────────
+export const TODAY_SESSIONS = [
+ { time: '09:30', team: 'Team Falcon', type: 'Sprint Review Coaching', duration: '90 min', teamId: 'tf' },
+ { time: '11:30', team: 'Team Nova', type: '1:1 with Scrum Master', duration: '45 min', teamId: 'tn' },
+ { time: '14:00', team: 'Team Vortex', type: 'Impediment Workshop', duration: '120 min', teamId: 'tv' },
+];
diff --git a/src/views/AIAssistant.jsx b/src/views/AIAssistant.jsx
new file mode 100644
index 0000000..62d7e1e
--- /dev/null
+++ b/src/views/AIAssistant.jsx
@@ -0,0 +1,182 @@
+import React, { useState } from 'react';
+import { BrainCircuit, Loader2, Zap, Copy, Check, ChevronDown } from 'lucide-react';
+
+const MODES = [
+ { id: 'questions', label: 'Coaching Questions', prompt: (ctx) => `Generate 6 powerful, open-ended coaching questions for an agile coaching session. Context: ${ctx}. Make them specific, challenging, and designed to provoke reflection and action. Numbered list.` },
+ { id: 'prep', label: 'Session Prep', prompt: (ctx) => `Create a structured session preparation plan for: ${ctx}. Include: 1) Session objective, 2) 3 key outcomes to achieve, 3) Suggested agenda (time-boxed), 4) Potential resistance to anticipate, 5) Opening question.` },
+ { id: 'workshop', label: 'Workshop Agenda', prompt: (ctx) => `Design a 2-hour workshop agenda for: ${ctx}. Include: Opening check-in, main activities with timing, facilitation techniques, expected outputs, and closing. Be specific and practical.` },
+ { id: 'summary', label: 'Progress Summary', prompt: (ctx) => `Write a concise executive progress summary for a coaching engagement. Context: ${ctx}. Include: Overall health assessment, key achievements this month, risks and blockers, and recommended next steps. Professional tone, data-driven where possible.` },
+ { id: 'ceo', label: 'CEO Report Draft', prompt: (ctx) => `Draft a monthly CEO steering committee coaching progress report. Context: ${ctx}. Structure: Executive Summary (2-3 sentences), Programme Health (traffic light with rationale), Key Milestones Achieved, Risks & Mitigations, Recommendations for Executive Action. Formal, concise, outcome-focused.` },
+];
+
+const CONTEXT_PRESETS = [
+ { label: 'Meridian Telco — SAFe Transformation, 14 teams, Scale phase', value: 'Client: Meridian Telco. Programme: SAFe 6.0 Transformation. Phase: Scale. 14 product teams across 3 BUs (Digital Products, Network Operations, Customer Experience). 6 months in. Average maturity: 2.4/4.0. Key blockers: PO empowerment gaps, platform team dependencies, tech debt accumulation.' },
+ { label: 'BankFirst — Digital Transformation, Vision & Alignment', value: 'Client: BankFirst Group. Programme: Digital Transformation. Phase: Vision & Alignment. 6 C-suite stakeholders. Key tension: CFO cost-out agenda vs CDO revenue growth agenda. Core banking modernisation deferred to Year 2. Talent gap identified.' },
+ { label: 'Team Falcon — Sprint Retro coaching, Developing maturity', value: 'Team: Falcon. BU: Digital Products. Maturity: Developing (2.4/4.0). Sprint: PI 4/Sprint 3. Blockers: PO lacks scope authority, 3 overdue actions, tech debt backlog invisible at ART level. 5 open actions.' },
+];
+
+async function callAI(prompt) {
+ try {
+ const res = await fetch('/api/gemini', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt, context: '' }),
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.text || '';
+ } catch (e) {
+ const GEMINI_KEY = 'AIzaSyDZn7tv1D3n2zuns8uaXIqp_z1FZTeVyKI';
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_KEY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ systemInstruction: { parts: [{ text: 'You are an expert agile and enterprise coaching assistant for Falah Consulting. Be concise, practical, and outcome-focused.' }] },
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
+ generationConfig: { temperature: 0.75, maxOutputTokens: 800 },
+ }),
+ });
+ const d = await r.json();
+ return d?.candidates?.[0]?.content?.parts?.[0]?.text || 'No response from AI.';
+ }
+}
+
+export default function AIAssistant({ persona, engagement }) {
+ const [mode, setMode] = useState(MODES[0]);
+ const [context, setContext] = useState('');
+ const [output, setOutput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const [history, setHistory] = useState([]);
+
+ const handleGenerate = async () => {
+ if (!context.trim()) return;
+ setLoading(true);
+ setOutput('');
+ const prompt = mode.prompt(context);
+ const text = await callAI(prompt);
+ setOutput(text);
+ setHistory(prev => [{ mode: mode.label, context: context.slice(0, 80) + (context.length > 80 ? '...' : ''), output: text, ts: new Date().toLocaleTimeString() }, ...prev.slice(0, 4)]);
+ setLoading(false);
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ AI Coach Assistant
+
+
Generate coaching questions, session plans, summaries, and reports.
+
+
+
+ {/* Left — Input */}
+
+ {/* Mode selector */}
+
+
Mode
+
+ {MODES.map(m => (
+ setMode(m)}
+ className={`text-xs font-medium px-3 py-1.5 rounded-md border transition-colors ${mode.id === m.id ? 'bg-navy-700 text-white border-navy-700' : 'bg-white text-gray-600 border-gray-200 hover:border-navy-300 hover:text-navy-700'}`}
+ >
+ {m.label}
+
+ ))}
+
+
+
+ {/* Context */}
+
+
+ Context
+ or choose a preset below
+
+
+
+
+
+ {loading ? <> Generating…> : <> Generate {mode.label}>}
+
+
+
+ {/* Right — Output */}
+
+
+
+ {mode.label}
+ {output && (
+
+ {copied ? <> Copied> : <> Copy>}
+
+ )}
+
+
+ {loading ? (
+
+ Thinking…
+
+ ) : output ? (
+
{output}
+ ) : (
+
Output will appear here…
+ )}
+
+
+
+ {/* History */}
+ {history.length > 0 && (
+
+
+ Recent
+
+
+ {history.map((h, i) => (
+
setOutput(h.output)}
+ className="w-full flex items-start gap-3 px-4 py-3 hover:bg-gray-50 text-left transition-colors">
+
+
{h.mode}
+
{h.context}
+
+ {h.ts}
+
+ ))}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/views/Dashboard.jsx b/src/views/Dashboard.jsx
new file mode 100644
index 0000000..6db90d2
--- /dev/null
+++ b/src/views/Dashboard.jsx
@@ -0,0 +1,287 @@
+import React from 'react';
+import { Calendar, Clock, AlertTriangle, CheckCircle, ArrowRight, Users, TrendingUp, Zap } from 'lucide-react';
+import { TEAMS, SESSIONS, ACTIONS, TODAY_SESSIONS, WORKSHOPS, PORTFOLIO_SUMMARY, ENGAGEMENTS } from '../data/mock.js';
+
+const StatusDot = ({ status }) => {
+ const map = { healthy: 'bg-green-500', 'at-risk': 'bg-amber-400', blocked: 'bg-red-500' };
+ return ;
+};
+
+const MaturityBadge = ({ level }) => {
+ const map = {
+ Foundation: 'bg-gray-100 text-gray-600',
+ Developing: 'bg-blue-50 text-blue-700',
+ Performing: 'bg-emerald-50 text-emerald-700',
+ Leading: 'bg-navy-50 text-navy-700',
+ };
+ return (
+
+ {level}
+
+ );
+};
+
+/* ── Marcus dashboard ── */
+function MarcusDashboard({ engagement, navigate }) {
+ const allTeams = TEAMS;
+ const flagged = allTeams.filter(t => t.status !== 'healthy');
+ const openActions = ACTIONS.filter(a => a.status === 'open' || a.status === 'overdue');
+ const overdueCount = ACTIONS.filter(a => a.status === 'overdue').length;
+
+ return (
+
+ {/* Header */}
+
+
Good morning, Marcus
+
+ {new Date().toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' })}
+ {engagement && · {engagement.client} — {engagement.name} }
+
+
+
+ {/* Summary stats */}
+
+ {[
+ { label: 'Teams Coaching', value: allTeams.length, icon: Users, color: 'text-navy-700' },
+ { label: 'Flagged Teams', value: flagged.length, icon: AlertTriangle, color: 'text-amber-600' },
+ { label: 'Open Actions', value: openActions.length, icon: CheckCircle, color: 'text-blue-600' },
+ { label: 'Overdue Items', value: overdueCount, icon: Clock, color: 'text-red-600' },
+ ].map(({ label, value, icon: Icon, color }) => (
+
+ ))}
+
+
+
+ {/* Today's sessions */}
+
+
+
+ Today's Sessions
+
+ {TODAY_SESSIONS.length} scheduled
+
+
+ {TODAY_SESSIONS.map((s, i) => {
+ const team = TEAMS.find(t => t.id === s.teamId);
+ return (
+
navigate('team', { team })}
+ className="w-full flex items-start gap-4 px-5 py-3.5 hover:bg-gray-50 transition-colors text-left group"
+ >
+ {s.time}
+
+
{s.team}
+
{s.type} · {s.duration}
+
+ {team && }
+
+ );
+ })}
+
+
+
+ {/* Flagged teams */}
+
+
+
+ Needs Attention
+
+
navigate('portfolio')} className="text-xs text-navy-700 hover:underline">View all →
+
+
+ {flagged.slice(0, 5).map(team => (
+
navigate('team', { team })}
+ className="w-full flex items-center gap-4 px-5 py-3.5 hover:bg-gray-50 transition-colors text-left group"
+ >
+
+
+
{team.name}
+
{team.bu} · {team.openActions} open actions
+
+
+
+ ))}
+
+
+
+
+ {/* Overdue actions */}
+ {overdueCount > 0 && (
+
+
+
+ {overdueCount} Overdue Action{overdueCount > 1 ? 's' : ''}
+
+
+
+ {ACTIONS.filter(a => a.status === 'overdue').map(a => {
+ const team = TEAMS.find(t => t.id === a.teamId);
+ return (
+
+
+
{a.text}
+
{team?.name} · Owner: {a.owner} · Due {a.dueDate}
+
+
Overdue
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
+
+/* ── Priya dashboard ── */
+function PriyaDashboard({ engagement, navigate }) {
+ const workshops = WORKSHOPS.filter(w => w.engagementId === engagement?.id);
+ return (
+
+
+
Good morning, Priya
+
+ {new Date().toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' })}
+ {engagement && · {engagement.client} — {engagement.name} }
+
+
+
+
+ {[
+ { label: 'Active Engagement', value: '1', icon: Zap, color: 'text-navy-700' },
+ { label: 'Workshops Run', value: workshops.length, icon: Calendar, color: 'text-blue-600' },
+ { label: 'Stakeholders', value: '6', icon: Users, color: 'text-emerald-600' },
+ { label: 'Open Questions', value: '5', icon: AlertTriangle, color: 'text-amber-600' },
+ ].map(({ label, value, icon: Icon, color }) => (
+
+ ))}
+
+
+ {/* Engagement phase */}
+ {engagement && (
+
+
+
+
Current Phase
+
{engagement.phase}
+
{engagement.description}
+
+
{engagement.phase}
+
+
+ )}
+
+ {/* Recent workshop */}
+ {workshops[0] && (
+
+
+
Latest Workshop
+ navigate('workshops')} className="text-xs text-navy-700 hover:underline">View all →
+
+
+
+
+
{workshops[0].name}
+
{workshops[0].date} · {workshops[0].duration} min · {workshops[0].stakeholders.length} stakeholders
+
+
Completed
+
+
{workshops[0].aiSummary}
+
+ ✓ {workshops[0].captures.filter(c=>c.type==='decision').length} decisions
+ ⚡ {workshops[0].captures.filter(c=>c.type==='tension').length} tensions
+ ? {workshops[0].captures.filter(c=>c.type==='question').length} open questions
+
+
+
+ )}
+
+ );
+}
+
+/* ── David dashboard ── */
+function DavidDashboard({ navigate }) {
+ const ps = PORTFOLIO_SUMMARY;
+ return (
+
+
+
Good morning, David
+
+ {new Date().toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' })} · Portfolio View
+
+
+
+
+ {[
+ { label: 'Engagements', value: ps.totalEngagements, icon: Zap, color: 'text-navy-700' },
+ { label: 'Teams Coached', value: ps.totalTeams, icon: Users, color: 'text-blue-600' },
+ { label: 'Active Coaches', value: ps.totalCoaches, icon: TrendingUp, color: 'text-emerald-600' },
+ { label: 'Portfolio Health', value: ps.overallHealth.toUpperCase(), icon: AlertTriangle, color: ps.overallHealth === 'green' ? 'text-green-600' : 'text-amber-600' },
+ ].map(({ label, value, icon: Icon, color }) => (
+
+ ))}
+
+
+
+
+
Monthly Insights
+ navigate('reports')} className="text-xs text-navy-700 hover:underline">Generate CEO Report →
+
+
+ {ps.monthlyInsights.map((insight, i) => (
+
+ ))}
+
+
+
+
+
+
Engagement Health
+
+
+ {ENGAGEMENTS.map(e => {
+ const h = ps.engagementHealth[e.id];
+ return (
+
+
+
+
{e.client} — {e.name}
+
{e.phase} · {e.industry}
+
+
+
{h.teamsHealthy} healthy
+
{h.teamsAtRisk} at risk
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+/* ── Router ── */
+export default function Dashboard({ persona, engagement, navigate }) {
+ if (persona.id === 'priya') return ;
+ if (persona.id === 'david') return ;
+ return ;
+}
diff --git a/src/views/Playbooks.jsx b/src/views/Playbooks.jsx
new file mode 100644
index 0000000..91811fd
--- /dev/null
+++ b/src/views/Playbooks.jsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { Search, BookOpen, Clock, ChevronRight } from 'lucide-react';
+import { PLAYBOOKS } from '../data/mock.js';
+
+const CATEGORIES = ['All', 'Agile', 'Digital Transformation', 'Facilitation', 'Enterprise'];
+const LEVELS = ['All', 'Foundation', 'Intermediate', 'Advanced'];
+
+const LEVEL_STYLE = {
+ Foundation: 'bg-gray-100 text-gray-600',
+ Intermediate: 'bg-blue-50 text-blue-700',
+ Advanced: 'bg-navy-50 text-navy-700',
+ 'All Levels': 'bg-emerald-50 text-emerald-700',
+ Template: 'bg-purple-50 text-purple-700',
+};
+
+const CAT_STYLE = {
+ Agile: 'bg-blue-50 text-blue-700',
+ 'Digital Transformation': 'bg-emerald-50 text-emerald-700',
+ Facilitation: 'bg-amber-50 text-amber-700',
+ Enterprise: 'bg-navy-50 text-navy-700',
+};
+
+function PlaybookCard({ pb }) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen(o => !o)}
+ className="w-full flex items-start gap-4 p-5 text-left group"
+ >
+
+
+
+
+
+
+ {pb.tag}
+ {pb.level}
+ {pb.duration}
+
+
+
+
+ {open && (
+
+
{pb.description}
+
+ Last updated {pb.lastUpdated}
+ Open Playbook →
+
+
+ )}
+
+ );
+}
+
+export default function Playbooks() {
+ const [search, setSearch] = useState('');
+ const [category, setCategory] = useState('All');
+ const [level, setLevel] = useState('All');
+
+ let books = PLAYBOOKS;
+ if (search) books = books.filter(p => p.title.toLowerCase().includes(search.toLowerCase()) || p.description.toLowerCase().includes(search.toLowerCase()) || p.tag.toLowerCase().includes(search.toLowerCase()));
+ if (category !== 'All') books = books.filter(p => p.category === category);
+ if (level !== 'All') books = books.filter(p => p.level === level || p.level === 'All Levels');
+
+ return (
+
+
+
Playbook Library
+
Agile frameworks, DT models, and facilitation exercises.
+
+
+ {/* Filters */}
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search playbooks…"
+ className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-navy-700 focus:border-navy-700 placeholder-gray-400" />
+
+
setCategory(e.target.value)}
+ className="text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-navy-700 cursor-pointer">
+ {CATEGORIES.map(c => {c === 'All' ? 'Category: All' : c} )}
+
+
setLevel(e.target.value)}
+ className="text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-navy-700 cursor-pointer">
+ {LEVELS.map(l => {l === 'All' ? 'Level: All' : l} )}
+
+
+
+ {/* Category tabs */}
+
+ {CATEGORIES.map(c => (
+ setCategory(c)}
+ className={`text-xs font-medium px-3 py-1.5 rounded-full border transition-colors ${category === c ? 'bg-navy-700 text-white border-navy-700' : 'bg-white text-gray-600 border-gray-200 hover:border-navy-200'}`}>
+ {c}
+
+ ))}
+
+
+ {/* Grid */}
+ {books.length === 0 ? (
+
No playbooks match your filters.
+ ) : (
+
+ )}
+
Showing {books.length} of {PLAYBOOKS.length} playbooks
+
+ );
+}
diff --git a/src/views/Reports.jsx b/src/views/Reports.jsx
new file mode 100644
index 0000000..86acc78
--- /dev/null
+++ b/src/views/Reports.jsx
@@ -0,0 +1,201 @@
+import React, { useState } from 'react';
+import { BarChart2, TrendingUp, AlertTriangle, Users, Zap, Loader2, Copy, Check } from 'lucide-react';
+import { ENGAGEMENTS, PORTFOLIO_SUMMARY, TEAMS } from '../data/mock.js';
+
+const HEALTH_STYLE = {
+ green: { dot: 'bg-green-500', badge: 'bg-green-50 text-green-700', label: 'On Track' },
+ amber: { dot: 'bg-amber-400', badge: 'bg-amber-50 text-amber-700', label: 'At Risk' },
+ red: { dot: 'bg-red-500', badge: 'bg-red-50 text-red-600', label: 'Critical' },
+};
+
+async function callAI(prompt) {
+ try {
+ const res = await fetch('/api/gemini', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt, context: '' }),
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.text || '';
+ } catch (e) {
+ const KEY = 'AIzaSyDZn7tv1D3n2zuns8uaXIqp_z1FZTeVyKI';
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${KEY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ systemInstruction: { parts: [{ text: 'You are a senior enterprise coaching expert at Falah Consulting. Write formal, data-driven reports for C-suite audiences.' }] },
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
+ generationConfig: { temperature: 0.6, maxOutputTokens: 1200 },
+ }),
+ });
+ const d = await r.json();
+ return d?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+ }
+}
+
+/* Mini bar for maturity distribution */
+function MaturityBar({ label, count, total, color }) {
+ const pct = total > 0 ? (count / total) * 100 : 0;
+ return (
+
+ );
+}
+
+export default function Reports({ persona }) {
+ const ps = PORTFOLIO_SUMMARY;
+ const [reportLoading, setReportLoading] = useState(false);
+ const [report, setReport] = useState('');
+ const [copied, setCopied] = useState(false);
+
+ // Maturity distribution for Meridian
+ const maturityDist = {
+ Leading: TEAMS.filter(t => t.maturityLevel === 'Leading').length,
+ Performing: TEAMS.filter(t => t.maturityLevel === 'Performing').length,
+ Developing: TEAMS.filter(t => t.maturityLevel === 'Developing').length,
+ Foundation: TEAMS.filter(t => t.maturityLevel === 'Foundation').length,
+ };
+
+ const generateReport = async () => {
+ setReportLoading(true);
+ const context = `Portfolio: 3 active engagements — Meridian Telco SAFe Transformation (14 teams, Scale phase, avg maturity 2.7/4.0, 2 blocked teams), BankFirst Digital Transformation (Vision phase, CFO/CDO tension unresolved), Nexus Energy Agile at Scale (Foundation phase, on track). Overall portfolio health: Amber. Key risks: Meridian platform dependency blocking 2 teams, BankFirst executive alignment gap, Nexus 2-week schedule slippage. Wins: Teams Zenith and Atlas reached Leading maturity. 4 coaches deployed.`;
+ const prompt = `Draft a monthly CEO steering committee coaching progress report for Falah Consulting.\n\nContext: ${context}\n\nStructure:\n1. EXECUTIVE SUMMARY (2-3 sentences)\n2. PORTFOLIO HEALTH OVERVIEW (traffic light per engagement with 1-line rationale)\n3. KEY ACHIEVEMENTS THIS MONTH (3-4 bullet points, specific and measurable)\n4. RISKS & MITIGATIONS (top 3, each with: Risk / Impact / Mitigation action)\n5. RECOMMENDATIONS FOR EXECUTIVE ACTION (2-3 actionable recommendations for the steering committee)\n\nTone: Formal, concise, outcome-focused. Suitable for a Group CEO audience.`;
+ const text = await callAI(prompt);
+ setReport(text);
+ setReportLoading(false);
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(report);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+
Portfolio Overview
+
Cross-engagement health · David Walsh · Partner
+
+
+ {/* Summary stats */}
+
+ {[
+ { label: 'Engagements', value: ps.totalEngagements, icon: Zap },
+ { label: 'Teams', value: ps.totalTeams, icon: Users },
+ { label: 'Coaches', value: ps.totalCoaches, icon: TrendingUp },
+ { label: 'Portfolio Health', value: ps.overallHealth === 'green' ? 'ON TRACK' : ps.overallHealth === 'amber' ? 'AT RISK' : 'CRITICAL', icon: BarChart2 },
+ ].map(({ label, value, icon: Icon }) => (
+
+ ))}
+
+
+
+ {/* Engagement health */}
+
+
+
Engagement Health
+
+
+ {ENGAGEMENTS.map(e => {
+ const h = ps.engagementHealth[e.id];
+ const hs = HEALTH_STYLE[h.health] || HEALTH_STYLE.amber;
+ return (
+
+
+
+
{e.client}
+
{e.name} · {e.phase}
+
+
{hs.label}
+
+
+ {h.teamsHealthy} healthy
+ {h.teamsAtRisk > 0 && {h.teamsAtRisk} at risk }
+ {h.teamsBlocked > 0 && {h.teamsBlocked} blocked }
+ {h.avgMaturity && Avg maturity: {h.avgMaturity}/4.0 }
+
+
+ );
+ })}
+
+
+
+ {/* Meridian maturity distribution */}
+
+
+
Meridian — Maturity Distribution
+
14 teams across 3 BUs
+
+
+
+
+
+
+
+
+
+ Systemic pattern: All 3 blocked/Foundation teams share the same platform dependency blocker. Recommend ART-level escalation.
+
+
+
+
+
+ {/* Monthly insights */}
+
+
+
Portfolio Insights
+
+
+ {ps.monthlyInsights.map((insight, i) => (
+
+ ))}
+
+
+
+ {/* CEO report generator */}
+
+
+
+
+ CEO Steering Committee Report
+
+
AI-generated from portfolio data — ready to edit and distribute
+
+ {report && (
+
+ {copied ? <> Copied> : <> Copy Report>}
+
+ )}
+
+
+ {report ? (
+
{report}
+ ) : (
+
Generate a monthly progress report for the Group CEO — structured narrative covering portfolio health, achievements, risks, and executive recommendations.
+ )}
+
+ {reportLoading ? <> Drafting report…> : <> {report ? 'Regenerate Report' : 'Draft CEO Report'}>}
+
+
+
+
+ );
+}
diff --git a/src/views/SessionCapture.jsx b/src/views/SessionCapture.jsx
new file mode 100644
index 0000000..91a6e56
--- /dev/null
+++ b/src/views/SessionCapture.jsx
@@ -0,0 +1,252 @@
+import React, { useState, useRef, useCallback } from 'react';
+import { ArrowLeft, Zap, Loader2, Save, Tag, AlertCircle, HelpCircle, CheckCircle, TrendingDown, ChevronRight } from 'lucide-react';
+
+const TAG_TYPES = [
+ { type: 'blocker', label: '⛔ Blocker', color: 'bg-red-50 text-red-700 border-red-200 hover:bg-red-100' },
+ { type: 'action', label: '✓ Action', color: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100' },
+ { type: 'decision', label: '● Decision', color: 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100' },
+ { type: 'question', label: '? Question', color: 'bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100' },
+ { type: 'tension', label: '⚡ Tension', color: 'bg-purple-50 text-purple-700 border-purple-200 hover:bg-purple-100' },
+];
+
+const SESSION_TYPES = [
+ 'Sprint Retrospective Coaching',
+ 'Sprint Review Coaching',
+ 'Team Health Check',
+ 'Impediment Removal Workshop',
+ '1:1 with Scrum Master',
+ '1:1 with Product Owner',
+ 'PI Planning Coaching',
+ 'ART Sync Coaching',
+];
+
+async function callAI(prompt, context) {
+ try {
+ const res = await fetch('/api/gemini', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt, context }),
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.text || '';
+ } catch (e) {
+ // Fallback to direct Gemini call if serverless not available
+ const GEMINI_KEY = 'AIzaSyDZn7tv1D3n2zuns8uaXIqp_z1FZTeVyKI';
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_KEY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ systemInstruction: { parts: [{ text: 'You are an expert agile coaching assistant for Falah Consulting. Respond concisely and practically.' }] },
+ contents: [{ role: 'user', parts: [{ text: `Context: ${context}\n\n${prompt}` }] }],
+ generationConfig: { temperature: 0.7, maxOutputTokens: 512 },
+ }),
+ });
+ const d = await r.json();
+ return d?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+ }
+}
+
+export default function SessionCapture({ team, navigate }) {
+ const [sessionType, setSessionType] = useState(SESSION_TYPES[0]);
+ const [notes, setNotes] = useState('');
+ const [tagged, setTagged] = useState([]);
+ const [activeTag, setActiveTag] = useState(null);
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiSummary, setAiSummary] = useState('');
+ const [aiQuestions, setAiQuestions] = useState('');
+ const [qLoading, setQLoading] = useState(false);
+ const [saved, setSaved] = useState(false);
+ const textRef = useRef(null);
+
+ const teamName = team?.name || 'Unknown Team';
+ const teamContext = team ? `Team: ${team.name}, BU: ${team.bu}, Maturity: ${team.maturityLevel} (${team.maturityScore}/4), Sprint: ${team.sprint}, Open actions: ${team.openActions}` : '';
+
+ // Tag a selected note line
+ const tagSelection = useCallback(() => {
+ if (!activeTag || !textRef.current) return;
+ const start = textRef.current.selectionStart;
+ const end = textRef.current.selectionEnd;
+ const selected = notes.slice(start, end).trim();
+ if (!selected) return;
+ setTagged(prev => [...prev, { type: activeTag, text: selected, id: Date.now() }]);
+ }, [activeTag, notes]);
+
+ const removeTag = (id) => setTagged(prev => prev.filter(t => t.id !== id));
+
+ const generateQuestions = async () => {
+ setQLoading(true);
+ const prompt = `Generate 5 targeted coaching questions for a ${sessionType} session with ${teamName}. Each question should be open-ended, specific to their situation, and push the team to reflect and act. Format as a numbered list.`;
+ const text = await callAI(prompt, teamContext);
+ setAiQuestions(text);
+ setQLoading(false);
+ };
+
+ const generateSummary = async () => {
+ setAiLoading(true);
+ const tagStr = tagged.map(t => `[${t.type.toUpperCase()}] ${t.text}`).join('\n');
+ const prompt = `Write a concise coaching session summary (3–5 sentences) based on these session notes and captured items. End with a "Recommended Next Intervention" sentence.\n\nSession Notes:\n${notes}\n\nCaptured Items:\n${tagStr}`;
+ const text = await callAI(prompt, teamContext);
+ setAiSummary(text);
+ setAiLoading(false);
+ };
+
+ const handleSave = () => {
+ setSaved(true);
+ setTimeout(() => navigate('team', { team }), 1200);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
navigate('team', { team })} className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-navy-700 transition-colors">
+ {teamName}
+
+
+
New Session
+
+
+
+ {new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
+
+
+ {saved ? <> Saved> : <> Save Session>}
+
+
+
+
+
+
+ {/* Left — Notes + Tags */}
+
+ {/* Session type */}
+
+ Session Type
+ setSessionType(e.target.value)}
+ className="w-full text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-800 focus:outline-none focus:ring-1 focus:ring-navy-700">
+ {SESSION_TYPES.map(t => {t} )}
+
+
+
+ {/* Notes */}
+
+
+ Session Notes
+ Select text → choose tag type to capture
+
+
+
+ {/* Tagged items */}
+ {tagged.length > 0 && (
+
+
+ Captured Items ({tagged.length})
+
+
+ {tagged.map(item => {
+ const t = TAG_TYPES.find(x => x.type === item.type);
+ return (
+
+ {t?.label}
+ {item.text}
+ removeTag(item.id)} className="text-gray-300 hover:text-red-400 shrink-0 text-xs">✕
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* Right — AI Panel */}
+
+ {/* Coaching questions */}
+
+
+
+ AI Coaching Questions
+
+
+
+ {aiQuestions ? (
+
{aiQuestions}
+ ) : (
+
Get AI-generated coaching questions tailored to {teamName}'s current situation.
+ )}
+
+ {qLoading ? <> Generating…> : <> Generate Questions>}
+
+
+
+
+ {/* AI Summary */}
+
+
+
+ AI Session Summary
+
+
+
+ {aiSummary ? (
+
{aiSummary}
+ ) : (
+
Generate a structured summary from your notes and tagged items after the session.
+ )}
+
+ {aiLoading ? <> Summarising…> : <> Generate Summary>}
+
+
+
+
+ {/* Context card */}
+
+
Team Context
+
+
Team: {team?.name}
+
BU: {team?.bu}
+
Maturity: {team?.maturityLevel} ({team?.maturityScore}/4)
+
Open Actions: {team?.openActions}
+
Last Session: {team?.lastSession}
+
+
+
+
+
+ );
+}
diff --git a/src/views/TeamDetail.jsx b/src/views/TeamDetail.jsx
new file mode 100644
index 0000000..fc3ed76
--- /dev/null
+++ b/src/views/TeamDetail.jsx
@@ -0,0 +1,235 @@
+import React, { useState } from 'react';
+import { ArrowLeft, Play, CheckCircle2, Clock, AlertTriangle, ChevronDown, ChevronUp, Calendar, Users, Zap } from 'lucide-react';
+import { SESSIONS, ACTIONS } from '../data/mock.js';
+
+const TAG_STYLE = {
+ blocker: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-100', label: 'Blocker', dot: 'bg-red-500' },
+ action: { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-100', label: 'Action', dot: 'bg-blue-500' },
+ decision: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-100', label: 'Decision', dot: 'bg-green-500' },
+ question: { bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-100', label: 'Question', dot: 'bg-amber-400' },
+ tension: { bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-100', label: 'Tension', dot: 'bg-purple-500' },
+};
+
+const MATURITY_COLOR = {
+ Foundation: { bar: 'bg-gray-400', text: 'text-gray-600', bg: 'bg-gray-100' },
+ Developing: { bar: 'bg-blue-500', text: 'text-blue-700', bg: 'bg-blue-50' },
+ Performing: { bar: 'bg-emerald-500', text: 'text-emerald-700', bg: 'bg-emerald-50' },
+ Leading: { bar: 'bg-navy-700', text: 'text-navy-700', bg: 'bg-navy-50' },
+};
+
+function MaturityBar({ score, level }) {
+ const c = MATURITY_COLOR[level] || MATURITY_COLOR.Foundation;
+ const pct = (score / 4) * 100;
+ return (
+
+
+ {level}
+ {score.toFixed(1)} / 4.0
+
+
+
+ );
+}
+
+function SessionCard({ session, navigate, team }) {
+ const [expanded, setExpanded] = useState(false);
+ const tagCounts = session.tags?.reduce((acc, t) => { acc[t.type] = (acc[t.type]||0)+1; return acc; }, {});
+
+ return (
+
+
setExpanded(e => !e)}
+ className="w-full flex items-center gap-4 px-5 py-4 hover:bg-gray-50 transition-colors text-left"
+ >
+
+
+
+
+
{session.type}
+
{session.date} · {session.coach} · {session.duration} min
+
+
+ {tagCounts?.blocker && {tagCounts.blocker} blocker{tagCounts.blocker>1?'s':''} }
+ {tagCounts?.action && {tagCounts.action} action{tagCounts.action>1?'s':''} }
+ {tagCounts?.decision && {tagCounts.decision} decision{tagCounts.decision>1?'s':''} }
+
+ {expanded ? : }
+
+
+ {expanded && (
+
+ {/* AI Summary */}
+ {session.aiSummary && (
+
+
+ AI Summary
+
+
{session.aiSummary}
+
+ )}
+ {/* Tags */}
+ {session.tags?.length > 0 && (
+
+
Captured Items
+ {session.tags.map((tag, i) => {
+ const s = TAG_STYLE[tag.type] || TAG_STYLE.action;
+ return (
+
+
+
+
{s.label}
+
{tag.text}
+ {tag.owner &&
Owner: {tag.owner} · Due: {tag.due}
}
+
+
+ );
+ })}
+
+ )}
+
+ )}
+
+ );
+}
+
+export default function TeamDetail({ team, navigate }) {
+ if (!team) return (
+
+
No team selected.
+
navigate('portfolio')} className="mt-4 text-navy-700 text-sm hover:underline">← Back to Portfolio
+
+ );
+
+ const sessions = SESSIONS.filter(s => s.teamId === team.id);
+ const actions = ACTIONS.filter(a => a.teamId === team.id);
+ const open = actions.filter(a => a.status === 'open');
+ const overdue = actions.filter(a => a.status === 'overdue');
+ const done = actions.filter(a => a.status === 'done');
+
+ const mc = MATURITY_COLOR[team.maturityLevel] || MATURITY_COLOR.Foundation;
+
+ return (
+
+ {/* Back */}
+
navigate('portfolio')} className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-navy-700 transition-colors">
+ Back to Portfolio
+
+
+ {/* Team header */}
+
+
+
+
{team.name}
+
{team.bu} · {team.members} members · {team.sprint}
+
+
+
+ {team.status === 'at-risk' ? 'At Risk' : team.status.charAt(0).toUpperCase() + team.status.slice(1)}
+
+
navigate('session', { team, session: null })}
+ className="flex items-center gap-1.5 bg-navy-700 hover:bg-navy-800 text-white text-xs font-medium px-3 py-1.5 rounded-md transition-colors"
+ >
+ Start Session
+
+
+
+
+ {/* Maturity */}
+
+
+ {/* Maturity history sparkline */}
+ {team.maturityHistory?.length > 1 && (
+
+
Maturity Trend
+
+ {team.maturityHistory.map((h, i) => (
+
+
{h.score.toFixed(1)}
+
+
{h.month}
+
+ ))}
+
+ +{(team.maturityScore - team.maturityHistory[0].score).toFixed(1)} from start
+
+
+
+ )}
+
+
+ {/* Actions */}
+
+
+
+ Action Tracker
+
+
+ {overdue.length > 0 && {overdue.length} overdue }
+ {open.length > 0 && {open.length} open }
+ {done.length > 0 && {done.length} done }
+
+
+ {actions.length === 0 ? (
+
No actions recorded for this team.
+ ) : (
+
+ {actions.map(a => (
+
+
+ {a.status === 'done' && }
+
+
+
{a.text}
+
Owner: {a.owner} · Due {a.dueDate}
+
+
+ {a.status.charAt(0).toUpperCase() + a.status.slice(1)}
+
+
+ ))}
+
+ )}
+
+
+ {/* Session log */}
+
+
+
Session History
+ {sessions.length} session{sessions.length !== 1 ? 's' : ''}
+
+ {sessions.length === 0 ? (
+
+ No sessions recorded yet.
+
+ navigate('session', { team, session: null })}
+ className="mt-3 text-navy-700 text-sm hover:underline">Start the first session →
+
+ ) : (
+
+ {sessions.map(s => )}
+
+ )}
+
+
+ );
+}
diff --git a/src/views/TeamPortfolio.jsx b/src/views/TeamPortfolio.jsx
new file mode 100644
index 0000000..601030c
--- /dev/null
+++ b/src/views/TeamPortfolio.jsx
@@ -0,0 +1,184 @@
+import React, { useState } from 'react';
+import { Search, Filter, ArrowUpDown, ChevronRight } from 'lucide-react';
+import { TEAMS } from '../data/mock.js';
+
+const BU_LIST = ['All', 'Digital Products', 'Network Operations', 'Customer Experience'];
+const STATUS_LIST = ['All', 'healthy', 'at-risk', 'blocked'];
+const MATURITY_LIST = ['All', 'Foundation', 'Developing', 'Performing', 'Leading'];
+
+const STATUS_STYLE = {
+ healthy: { dot: 'bg-green-500', badge: 'bg-green-50 text-green-700', label: 'Healthy' },
+ 'at-risk':{ dot: 'bg-amber-400', badge: 'bg-amber-50 text-amber-700', label: 'At Risk' },
+ blocked: { dot: 'bg-red-500', badge: 'bg-red-50 text-red-600', label: 'Blocked' },
+};
+
+const MATURITY_COLOR = {
+ Foundation: 'bg-gray-100 text-gray-600',
+ Developing: 'bg-blue-50 text-blue-700',
+ Performing: 'bg-emerald-50 text-emerald-700',
+ Leading: 'bg-navy-50 text-navy-700',
+};
+
+/* Mini sparkline — SVG */
+function Sparkline({ history, size = 40 }) {
+ if (!history || history.length < 2) return null;
+ const max = 4, min = 0;
+ const w = size, h = 20;
+ const pts = history.map((p, i) => {
+ const x = (i / (history.length - 1)) * w;
+ const y = h - ((p.score - min) / (max - min)) * h;
+ return `${x},${y}`;
+ }).join(' ');
+ const lastScore = history[history.length - 1].score;
+ const firstScore = history[0].score;
+ const trending = lastScore >= firstScore;
+ return (
+
+
+
+ );
+}
+
+function TeamRow({ team, navigate }) {
+ const s = STATUS_STYLE[team.status] || STATUS_STYLE.healthy;
+ return (
+ navigate('team', { team })}
+ className="w-full flex items-center gap-4 px-5 py-3.5 hover:bg-gray-50 transition-colors text-left group"
+ >
+ {/* Status dot */}
+
+
+ {/* Team name + BU */}
+
+
{team.name}
+
{team.bu} · {team.members} members
+
+
+ {/* Maturity */}
+
+
+ {team.maturityLevel}
+
+
{team.maturityScore.toFixed(1)} / 4.0
+
+
+ {/* Sparkline */}
+
+
+
+
+ {/* Actions */}
+
+ {team.openActions > 0
+ ? 4 ? 'text-red-600' : 'text-amber-600'}`}>{team.openActions} actions
+ : —
+ }
+
+
+ {/* Last session */}
+
+ {team.lastSession}
+
+
+
+
+ );
+}
+
+export default function TeamPortfolio({ engagement, navigate }) {
+ const [search, setSearch] = useState('');
+ const [bu, setBu] = useState('All');
+ const [status, setStatus] = useState('All');
+ const [maturity, setMaturity] = useState('All');
+ const [sortBy, setSortBy] = useState('status');
+
+ let teams = TEAMS;
+ if (search) teams = teams.filter(t => t.name.toLowerCase().includes(search.toLowerCase()) || t.bu.toLowerCase().includes(search.toLowerCase()));
+ if (bu !== 'All') teams = teams.filter(t => t.bu === bu);
+ if (status !== 'All') teams = teams.filter(t => t.status === status);
+ if (maturity !== 'All') teams = teams.filter(t => t.maturityLevel === maturity);
+
+ const sortFns = {
+ status: (a, b) => { const o = { blocked: 0, 'at-risk': 1, healthy: 2 }; return (o[a.status]??3) - (o[b.status]??3); },
+ name: (a, b) => a.name.localeCompare(b.name),
+ maturity: (a, b) => a.maturityScore - b.maturityScore,
+ actions: (a, b) => b.openActions - a.openActions,
+ session: (a, b) => new Date(b.lastSession) - new Date(a.lastSession),
+ };
+ teams = [...teams].sort(sortFns[sortBy] || sortFns.status);
+
+ const counts = { blocked: TEAMS.filter(t=>t.status==='blocked').length, 'at-risk': TEAMS.filter(t=>t.status==='at-risk').length, healthy: TEAMS.filter(t=>t.status==='healthy').length };
+
+ return (
+
+ {/* Header */}
+
+
+
Team Portfolio
+
{engagement?.client} — {TEAMS.length} teams across 3 Business Units
+
+
+ {counts.blocked} Blocked
+ {counts['at-risk']} At Risk
+ {counts.healthy} Healthy
+
+
+
+ {/* Filters */}
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search teams…"
+ className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-navy-700 focus:border-navy-700 placeholder-gray-400"
+ />
+
+ {[
+ { label: 'BU', options: BU_LIST, value: bu, set: setBu },
+ { label: 'Status', options: STATUS_LIST, value: status, set: setStatus },
+ { label: 'Maturity', options: MATURITY_LIST, value: maturity, set: setMaturity },
+ ].map(({ label, options, value, set }) => (
+
set(e.target.value)}
+ className="text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-navy-700 cursor-pointer">
+ {options.map(o => {o === 'All' ? `${label}: All` : o} )}
+
+ ))}
+
setSortBy(e.target.value)}
+ className="text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-navy-700 cursor-pointer">
+ Sort: Status
+ Sort: Maturity
+ Sort: Actions
+ Sort: Last Session
+ Sort: Name
+
+
+
+ {/* Table */}
+
+ {/* Table header */}
+
+
+
Team / BU
+
Maturity
+
Trend
+
Actions
+
Last Session
+
+
+
+ {/* Rows */}
+
+ {teams.length === 0 ? (
+
No teams match your filters.
+ ) : (
+ teams.map(team =>
)
+ )}
+
+
+
+
Showing {teams.length} of {TEAMS.length} teams
+
+ );
+}
diff --git a/src/views/WorkshopCapture.jsx b/src/views/WorkshopCapture.jsx
new file mode 100644
index 0000000..4651e7d
--- /dev/null
+++ b/src/views/WorkshopCapture.jsx
@@ -0,0 +1,283 @@
+import React, { useState } from 'react';
+import { ArrowLeft, CheckCircle, AlertTriangle, HelpCircle, ArrowRight, Zap, Loader2, Share2, Copy, Check, ChevronRight } from 'lucide-react';
+import { WORKSHOPS } from '../data/mock.js';
+
+const CAPTURE_TYPES = [
+ { type: 'decision', label: '✓ Decision', color: 'bg-green-50 border-green-200 text-green-700 hover:bg-green-100' },
+ { type: 'tension', label: '⚡ Tension', color: 'bg-red-50 border-red-200 text-red-700 hover:bg-red-100' },
+ { type: 'question', label: '? Question', color: 'bg-amber-50 border-amber-200 text-amber-700 hover:bg-amber-100' },
+ { type: 'next-step', label: '→ Next Step', color: 'bg-blue-50 border-blue-200 text-blue-700 hover:bg-blue-100' },
+];
+
+const STYLE = {
+ decision: { bg: 'bg-green-50', border: 'border-green-100', text: 'text-green-700', dot: 'bg-green-500', label: 'Decision' },
+ tension: { bg: 'bg-red-50', border: 'border-red-100', text: 'text-red-700', dot: 'bg-red-500', label: 'Tension' },
+ question: { bg: 'bg-amber-50', border: 'border-amber-100', text: 'text-amber-700', dot: 'bg-amber-400', label: 'Question' },
+ 'next-step':{ bg: 'bg-blue-50', border: 'border-blue-100', text: 'text-blue-700', dot: 'bg-blue-500', label: 'Next Step' },
+};
+
+async function callAI(prompt) {
+ try {
+ const res = await fetch('/api/gemini', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt, context: '' }),
+ });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ return data.text || '';
+ } catch (e) {
+ const KEY = 'AIzaSyDZn7tv1D3n2zuns8uaXIqp_z1FZTeVyKI';
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${KEY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ systemInstruction: { parts: [{ text: 'You are an expert executive workshop facilitator. Be concise, structured, and practical.' }] },
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
+ generationConfig: { temperature: 0.7, maxOutputTokens: 800 },
+ }),
+ });
+ const d = await r.json();
+ return d?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+ }
+}
+
+export default function WorkshopCapture({ workshop: initialWorkshop, navigate }) {
+ const workshop = initialWorkshop || WORKSHOPS[0]; // default to latest
+ const [liveNote, setLiveNote] = useState('');
+ const [activeType, setActiveType] = useState('decision');
+ const [captures, setCaptures] = useState(workshop?.captures || []);
+ const [aiInput, setAiInput] = useState('');
+ const [aiResponse, setAiResponse] = useState('');
+ const [aiLoading, setAiLoading] = useState(false);
+ const [summaryLoading, setSummaryLoading] = useState(false);
+ const [summary, setSummary] = useState(workshop?.aiSummary || '');
+ const [copied, setCopied] = useState(false);
+ const [view, setView] = useState(initialWorkshop ? 'summary' : 'live'); // 'live' | 'summary'
+
+ const addCapture = () => {
+ if (!liveNote.trim()) return;
+ setCaptures(prev => [...prev, { type: activeType, text: liveNote.trim(), timestamp: new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) }]);
+ setLiveNote('');
+ };
+
+ const removeCapture = (idx) => setCaptures(prev => prev.filter((_, i) => i !== idx));
+
+ const handleAiAssist = async () => {
+ if (!aiInput.trim()) return;
+ setAiLoading(true);
+ const prompt = `You are a workshop facilitator. Context: ${workshop?.objective || 'executive alignment workshop'}.\n\nQuestion from facilitator: ${aiInput}\n\nProvide a brief, practical facilitation move or technique (2-3 sentences max).`;
+ const text = await callAI(prompt);
+ setAiResponse(text);
+ setAiLoading(false);
+ setAiInput('');
+ };
+
+ const generateSummary = async () => {
+ setSummaryLoading(true);
+ const captureStr = captures.map(c => `[${c.type.toUpperCase()}] ${c.text}`).join('\n');
+ const prompt = `Write a structured workshop summary from these captured items:\n\n${captureStr}\n\nWorkshop objective: ${workshop?.objective || 'Executive alignment'}.\n\nFormat as:\nDECISIONS MADE:\n- ...\n\nOPEN QUESTIONS:\n- ...\n\nTENSIONS TO RESOLVE:\n- ...\n\nNEXT STEPS:\n- ...\n\nOVERALL ASSESSMENT: (1-2 sentences on alignment level and key risk)`;
+ const text = await callAI(prompt);
+ setSummary(text);
+ setSummaryLoading(false);
+ setView('summary');
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(summary);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ const groupedCaptures = CAPTURE_TYPES.map(t => ({
+ ...t,
+ items: captures.filter(c => c.type === t.type),
+ })).filter(g => g.items.length > 0);
+
+ return (
+
+ {/* Header */}
+
+
+
navigate('workshops')} className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-navy-700 transition-colors">
+ Workshops
+
+
+
{workshop?.name || 'New Workshop'}
+
+
+ setView(v => v === 'live' ? 'summary' : 'live')}
+ className="text-xs font-medium text-navy-700 border border-navy-200 bg-navy-50 hover:bg-navy-100 px-3 py-1.5 rounded-md transition-colors">
+ {view === 'live' ? 'View Summary' : '← Live Capture'}
+
+ {view === 'live' && (
+
+ {summaryLoading ? <> Summarising…> : <> Generate Summary>}
+
+ )}
+ {view === 'summary' && summary && (
+
+ {copied ? <> Copied> : <> Copy Summary>}
+
+ )}
+
+
+
+ {view === 'live' ? (
+
+ {/* Left — Live capture */}
+
+ {/* Objective */}
+ {workshop?.objective && (
+
+
Workshop Objective
+
{workshop.objective}
+
+ )}
+
+ {/* Input */}
+
+
+
Tag & Capture
+
+ {CAPTURE_TYPES.map(t => (
+ setActiveType(t.type)}
+ className={`text-xs font-medium px-2.5 py-1.5 rounded border transition-colors ${t.color} ${activeType === t.type ? 'ring-2 ring-offset-1 ring-navy-400' : ''}`}>
+ {t.label}
+
+ ))}
+
+
+
+
+
+
+ {/* Captures list */}
+
+ {captures.length === 0 ? (
+
+ Captures will appear here as you add them…
+
+ ) : (
+ captures.map((c, i) => {
+ const s = STYLE[c.type] || STYLE.decision;
+ return (
+
+
+
+
{s.label}
+ {c.timestamp &&
{c.timestamp} }
+
{c.text}
+ {c.stakeholder &&
— {c.stakeholder}
}
+
+
removeCapture(i)} className="text-gray-300 hover:text-red-400 text-xs shrink-0">✕
+
+ );
+ })
+ )}
+
+
+
+ {/* Right — AI Assist */}
+
+
+
+
+ Live AI Assist
+
+
+
+
setAiInput(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') handleAiAssist(); }}
+ placeholder="e.g. Two execs disagree on priorities…"
+ className="w-full text-sm border border-gray-200 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-navy-700 placeholder-gray-300" />
+
+ {aiLoading ? <> Thinking…> : 'Get facilitation move →'}
+
+ {aiResponse && (
+
+ {aiResponse}
+
+ )}
+
+
+
+ {/* Stakeholders */}
+ {workshop?.stakeholders && (
+
+
Stakeholders
+
+ {workshop.stakeholders.map(s => (
+ {s}
+ ))}
+
+
+ )}
+
+
+ ) : (
+ /* Summary view */
+
+ {summaryLoading ? (
+
+
+ Generating structured summary…
+
+ ) : summary ? (
+ <>
+
+
+
AI-Generated Workshop Summary
+ {workshop?.name}
+
+
{summary}
+
+
+ {/* Grouped captures */}
+ {groupedCaptures.map(group => {
+ const s = STYLE[group.type] || STYLE.decision;
+ return (
+
+
+
{s.label}s ({group.items.length})
+
+
+ {group.items.map((item, i) => (
+
+
+
+
{item.text}
+ {item.stakeholder &&
— {item.stakeholder} · {item.timestamp}
}
+ {item.owner &&
Owner: {item.owner} · Due: {item.due}
}
+
+
+ ))}
+
+
+ );
+ })}
+ >
+ ) : (
+
+
No summary yet. Go to Live Capture and click "Generate Summary".
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/views/WorkshopList.jsx b/src/views/WorkshopList.jsx
new file mode 100644
index 0000000..4132738
--- /dev/null
+++ b/src/views/WorkshopList.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { Calendar, Users, CheckCircle, AlertTriangle, Zap, ChevronRight, Plus } from 'lucide-react';
+import { WORKSHOPS } from '../data/mock.js';
+
+const CAPTURE_STYLE = {
+ decision: { icon: '✓', bg: 'bg-green-50', text: 'text-green-700', label: 'Decision' },
+ tension: { icon: '⚡', bg: 'bg-red-50', text: 'text-red-700', label: 'Tension' },
+ question: { icon: '?', bg: 'bg-amber-50', text: 'text-amber-700', label: 'Question' },
+ 'next-step':{ icon: '→', bg: 'bg-blue-50', text: 'text-blue-700', label: 'Next Step' },
+};
+
+export default function WorkshopList({ persona, engagement, navigate }) {
+ const workshops = WORKSHOPS.filter(w => w.engagementId === engagement?.id);
+
+ return (
+
+
+
+
Workshops
+
{engagement?.client} — {workshops.length} workshop{workshops.length !== 1 ? 's' : ''} recorded
+
+
navigate('workshop-live', { workshop: null })}
+ className="flex items-center gap-1.5 bg-navy-700 hover:bg-navy-800 text-white text-sm font-medium px-4 py-2 rounded-md transition-colors shrink-0"
+ >
+ New Workshop
+
+
+
+ {workshops.length === 0 ? (
+
+
+
No workshops recorded yet for this engagement.
+
navigate('workshop-live', { workshop: null })}
+ className="text-sm text-navy-700 hover:underline">Start your first workshop →
+
+ ) : (
+
+ {workshops.map(w => {
+ const decisions = w.captures.filter(c => c.type === 'decision');
+ const tensions = w.captures.filter(c => c.type === 'tension');
+ const questions = w.captures.filter(c => c.type === 'question');
+ const nextSteps = w.captures.filter(c => c.type === 'next-step');
+ return (
+
+
+
+
+
{w.name}
+
+ {w.date}
+ {w.stakeholders.length} stakeholders
+ {w.duration} min
+
+
+
+ Completed
+ navigate('workshop-live', { workshop: w })}
+ className="flex items-center gap-1 text-xs text-navy-700 border border-navy-200 bg-navy-50 hover:bg-navy-100 px-2.5 py-1 rounded transition-colors"
+ >
+ View
+
+
+
+
+
{w.aiSummary}
+
+ {/* Capture summary chips */}
+
+ {decisions.length > 0 && ✓ {decisions.length} decision{decisions.length>1?'s':''} }
+ {tensions.length > 0 && ⚡ {tensions.length} tension{tensions.length>1?'s':''} }
+ {questions.length > 0 && ? {questions.length} open question{questions.length>1?'s':''} }
+ {nextSteps.length > 0 && → {nextSteps.length} next step{nextSteps.length>1?'s':''} }
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 464848c..b709960 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,36 +1,38 @@
/** @type {import('tailwindcss').Config} */
export default {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
- sans: ['Inter', 'sans-serif'],
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ mono: ['JetBrains Mono', 'monospace'],
},
colors: {
- slate: {
- 850: '#151e2e',
- 900: '#0f172a',
- 950: '#020617',
- }
+ navy: {
+ 50: '#EEF2F7',
+ 100: '#D9E3EF',
+ 200: '#B3C7DF',
+ 300: '#8DAACF',
+ 400: '#678EBF',
+ 500: '#4172AF',
+ 600: '#2D5A99',
+ 700: '#1B2E4B',
+ 800: '#152440',
+ 900: '#0E1A34',
+ 950: '#070D1A',
+ },
},
animation: {
- 'fade-in': 'fadeIn 0.5s ease-out forwards',
- 'fade-in-up': 'fadeInUp 0.5s ease-out forwards',
+ 'fade-in': 'fadeIn 0.2s ease-out forwards',
+ 'fade-in-up': 'fadeInUp 0.25s ease-out forwards',
+ 'slide-in': 'slideIn 0.2s ease-out forwards',
},
keyframes: {
- fadeIn: {
- '0%': { opacity: '0' },
- '100%': { opacity: '1' },
- },
- fadeInUp: {
- '0%': { opacity: '0', transform: 'translateY(10px)' },
- '100%': { opacity: '1', transform: 'translateY(0)' },
- }
- }
+ fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
+ fadeInUp: { '0%': { opacity: '0', transform: 'translateY(8px)' },'100%': { opacity: '1', transform: 'translateY(0)' } },
+ slideIn: { '0%': { opacity: '0', transform: 'translateX(-8px)' },'100%': { opacity: '1', transform: 'translateX(0)' } },
+ },
},
},
plugins: [],
-}
+};