Completed login screen transition, and started the spec work on design file info

This commit is contained in:
2026-02-11 22:15:29 +00:00
parent 1a1f1f1938
commit 192d629125
22 changed files with 2357 additions and 390 deletions
-179
View File
@@ -1,179 +0,0 @@
{
"iterations": [
{
"iteration": 1,
"startedAt": "2026-02-11T01:23:53.316Z",
"endedAt": "2026-02-11T01:35:05.771Z",
"durationMs": 668162,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/SummaryView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 2,
"startedAt": "2026-02-11T01:35:10.653Z",
"endedAt": "2026-02-11T01:41:17.649Z",
"durationMs": 362952,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/ConsultationsView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 3,
"startedAt": "2026-02-11T01:41:22.593Z",
"endedAt": "2026-02-11T01:42:12.448Z",
"durationMs": 45945,
"toolsUsed": {},
"filesModified": [],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 4,
"startedAt": "2026-02-11T01:42:17.819Z",
"endedAt": "2026-02-11T01:50:07.083Z",
"durationMs": 465353,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/MedicationsView.tsx",
"src/index.css"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 5,
"startedAt": "2026-02-11T01:50:12.105Z",
"endedAt": "2026-02-11T01:58:48.582Z",
"durationMs": 512760,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/ProblemsView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 6,
"startedAt": "2026-02-11T01:58:53.428Z",
"endedAt": "2026-02-11T02:05:52.941Z",
"durationMs": 415696,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/InvestigationsView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 7,
"startedAt": "2026-02-11T02:05:57.951Z",
"endedAt": "2026-02-11T02:16:56.192Z",
"durationMs": 654352,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/DocumentsView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 8,
"startedAt": "2026-02-11T02:17:01.348Z",
"endedAt": "2026-02-11T02:30:01.815Z",
"durationMs": 776565,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/PMRInterface.tsx",
"src/components/views/ReferralsView.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 9,
"startedAt": "2026-02-11T02:30:07.269Z",
"endedAt": "2026-02-11T02:50:28.282Z",
"durationMs": 1217778,
"toolsUsed": {},
"filesModified": [
"goal.md",
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/App.tsx",
"src/components/ClinicalSidebar.tsx",
"src/components/LoginScreen.tsx",
"src/components/PMRInterface.tsx",
"src/components/views/ConsultationsView.tsx",
"src/components/views/SummaryView.tsx",
"src/contexts/AccessibilityContext.tsx"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
},
{
"iteration": 10,
"startedAt": "2026-02-11T02:50:32.603Z",
"endedAt": "2026-02-11T03:07:52.238Z",
"durationMs": 1036685,
"toolsUsed": {},
"filesModified": [
"Ralph/IMPLEMENTATION_PLAN.md",
"Ralph/progress.txt",
"src/components/ClinicalSidebar.tsx",
"src/components/MobileBottomNav.tsx",
"src/components/PMRInterface.tsx",
"src/components/PatientBanner.tsx",
"src/components/views/DocumentsView.tsx",
"src/components/views/InvestigationsView.tsx",
"src/components/views/MedicationsView.tsx",
"src/components/views/ProblemsView.tsx",
"src/hooks/useBreakpoint.ts"
],
"exitCode": 0,
"completionDetected": false,
"errors": []
}
],
"totalDurationMs": 6156248,
"struggleIndicators": {
"repeatedErrors": {},
"noProgressIterations": 0,
"shortIterations": 0
}
}
File diff suppressed because one or more lines are too long
+494
View File
@@ -0,0 +1,494 @@
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
// ─── Heartbeat generation ────────────────────────────────────────────────────
function generateHeartbeatPoints(
amplitude: number,
): { x: number; y: number }[] {
const points: { x: number; y: number }[] = [];
const steps = 200;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
let y = 0;
if (t >= 0.05 && t < 0.2) {
const pt = (t - 0.05) / 0.15;
y = 0.12 * Math.sin(pt * Math.PI);
} else if (t >= 0.25 && t < 0.32) {
const pt = (t - 0.25) / 0.07;
y = -0.1 * Math.sin(pt * Math.PI);
} else if (t >= 0.32 && t < 0.42) {
const pt = (t - 0.32) / 0.1;
y = 1.0 * Math.sin(pt * Math.PI);
} else if (t >= 0.42 && t < 0.5) {
const pt = (t - 0.42) / 0.08;
y = -0.25 * Math.sin(pt * Math.PI);
} else if (t >= 0.55 && t < 0.75) {
const pt = (t - 0.55) / 0.2;
y = 0.2 * Math.sin(pt * Math.PI);
}
points.push({ x: t, y: y * amplitude });
}
return points;
}
type Beat = { startFrame: number; widthPx: number; amplitude: number };
function buildBeats(fps: number): Beat[] {
const beats: Beat[] = [];
beats.push({ startFrame: Math.round(0.6 * fps), widthPx: 60, amplitude: 0.25 });
beats.push({ startFrame: Math.round(1.4 * fps), widthPx: 80, amplitude: 0.45 });
beats.push({ startFrame: Math.round(2.3 * fps), widthPx: 120, amplitude: 0.85 });
const normalStart = 3.2;
for (let i = 0; i < 1; i++) {
beats.push({
startFrame: Math.round((normalStart + i) * fps),
widthPx: 140,
amplitude: 1.0,
});
}
return beats;
}
// ─── Letter definitions ──────────────────────────────────────────────────────
const LETTERS: Record<string, { x: number; y: number }[]> = {
A: [
{ x: 0, y: 0 }, { x: 0.48, y: 1 }, { x: 0.53, y: 0.42 },
{ x: 0.6, y: 0.42 }, { x: 1, y: 0 },
],
N: [
{ x: 0, y: 0 }, { x: 0.12, y: 1 }, { x: 0.72, y: 0 },
{ x: 0.88, y: 1 }, { x: 1, y: 0 },
],
D: [
{ x: 0, y: 0 }, { x: 0.1, y: 1 }, { x: 0.5, y: 1 },
{ x: 0.85, y: 0.55 }, { x: 1, y: 0 },
],
R: [
{ x: 0, y: 0 }, { x: 0.1, y: 1 }, { x: 0.35, y: 1 },
{ x: 0.5, y: 0.6 }, { x: 0.55, y: 0.45 }, { x: 1, y: 0 },
],
E: [
{ x: 0, y: 0 }, { x: 0.1, y: 1 }, { x: 0.4, y: 1 },
{ x: 0.45, y: 0.5 }, { x: 0.65, y: 0.5 }, { x: 0.7, y: 0 },
{ x: 1, y: 0 },
],
W: [
{ x: 0, y: 0 }, { x: 0.05, y: 1 }, { x: 0.27, y: 0 },
{ x: 0.5, y: 0.65 }, { x: 0.73, y: 0 }, { x: 0.95, y: 1 },
{ x: 1, y: 0 },
],
C: [
{ x: 0, y: 0 }, { x: 0.08, y: 0.6 }, { x: 0.18, y: 1 },
{ x: 0.6, y: 1 }, { x: 0.8, y: 0.5 }, { x: 0.95, y: 0.1 },
{ x: 1, y: 0 },
],
H: [
{ x: 0, y: 0 }, { x: 0.1, y: 1 }, { x: 0.18, y: 0.5 },
{ x: 0.82, y: 0.5 }, { x: 0.9, y: 1 }, { x: 1, y: 0 },
],
L: [
{ x: 0, y: 0 }, { x: 0.12, y: 1 }, { x: 0.3, y: 1 },
{ x: 0.38, y: 0 }, { x: 1, y: 0 },
],
O: [
{ x: 0, y: 0 }, { x: 0.2, y: 0.85 }, { x: 0.35, y: 1 },
{ x: 0.65, y: 1 }, { x: 0.8, y: 0.85 }, { x: 1, y: 0 },
],
};
function interpolateLetterY(
points: { x: number; y: number }[],
t: number,
): number {
if (t <= points[0].x) return points[0].y;
if (t >= points[points.length - 1].x) return points[points.length - 1].y;
for (let i = 0; i < points.length - 1; i++) {
if (t >= points[i].x && t <= points[i + 1].x) {
const segT = (t - points[i].x) / (points[i + 1].x - points[i].x);
return points[i].y + (points[i + 1].y - points[i].y) * segT;
}
}
return 0;
}
// ─── Text layout ─────────────────────────────────────────────────────────────
const TEXT = "ANDREW CHARLWOOD";
const LETTER_WIDTH = 72;
const LETTER_GAP = 10;
const SPACE_WIDTH = 30;
const BASE_LEFT_INSET = 9;
const BASE_RIGHT_INSET = 0;
type LetterLayout = {
char: string;
startX: number;
endX: number;
startConnector: number;
endConnector: number;
};
type ConnectorProfile = { leftInset: number; rightInset: number };
const CONNECTOR_PROFILES: Record<string, ConnectorProfile> = {
C: { leftInset: 20, rightInset: 8 },
O: { leftInset: 17, rightInset: 7 },
D: { leftInset: 0, rightInset: 13 },
L: { leftInset: 5, rightInset: 0 },
E: { leftInset: 5, rightInset: 0 },
};
const DEFAULT_PROFILE: ConnectorProfile = { leftInset: 0, rightInset: 0 };
function layoutText(offsetX: number): LetterLayout[] {
const layout: LetterLayout[] = [];
let cursor = offsetX;
for (const char of TEXT) {
if (char === " ") {
cursor += SPACE_WIDTH;
continue;
}
const profile = CONNECTOR_PROFILES[char] ?? DEFAULT_PROFILE;
const startX = cursor;
const endX = cursor + LETTER_WIDTH;
layout.push({
char,
startX,
endX,
startConnector: startX + BASE_LEFT_INSET + profile.leftInset,
endConnector: endX - BASE_RIGHT_INSET - profile.rightInset,
});
cursor += LETTER_WIDTH + LETTER_GAP;
}
return layout;
}
function getTextTotalWidth(): number {
return (
TEXT.replace(/ /g, "").length * (LETTER_WIDTH + LETTER_GAP) -
LETTER_GAP +
(TEXT.split(" ").length - 1) * SPACE_WIDTH
);
}
// ─── Timing constants ────────────────────────────────────────────────────────
const TRACE_SPEED = 350;
const HEAD_SCREEN_RATIO = 1;
const FLAT_GAP_SECONDS = 0.5;
const HOLD_SECONDS = 1.25;
const COMP_FPS = 60;
// How long the dot/line takes to exit the right side after text finishes
const EXIT_SECONDS = 1.5;
// Pre-compute duration for export
const _beats = buildBeats(COMP_FPS);
const _lastBeat = _beats[_beats.length - 1];
const _lastBeatEndWX = (_lastBeat.startFrame / COMP_FPS) * TRACE_SPEED + _lastBeat.widthPx;
const _textStartWX = _lastBeatEndWX + FLAT_GAP_SECONDS * TRACE_SPEED;
const _totalTextW = getTextTotalWidth();
const _textEndWX = _textStartWX + _totalTextW;
const _textEndFrame = Math.round((_textEndWX / TRACE_SPEED) * COMP_FPS);
export const ECGCOMBINED_DURATION = _textEndFrame + Math.round(HOLD_SECONDS * COMP_FPS) + Math.round(EXIT_SECONDS * COMP_FPS);
// ─── Component ───────────────────────────────────────────────────────────────
export const ECGCombined = () => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const baselineY = height * 0.5;
const lineColor = "#00ff41";
const ecgMaxDeflection = height * 0.28;
const textMaxDeflection = height * 0.09;
const beats = buildBeats(fps);
// ── World-space text position ──
const lastBeat = beats[beats.length - 1];
const lastBeatEndWorldX = (lastBeat.startFrame / fps) * TRACE_SPEED + lastBeat.widthPx;
const textStartWorldX = lastBeatEndWorldX + FLAT_GAP_SECONDS * TRACE_SPEED;
const totalTextWidth = getTextTotalWidth();
const textEndWorldX = textStartWorldX + totalTextWidth;
const textLayout = layoutText(textStartWorldX); // world-space positions
// ── Final screen position: text centered when done ──
const desiredTextStartScreen = (width - totalTextWidth) / 2;
const finalHeadScreenX = desiredTextStartScreen + totalTextWidth;
const headScreenDuringEcg = HEAD_SCREEN_RATIO * width;
// ── Head position (world space, keeps moving past text) ──
const currentTime = frame / fps;
const headX = currentTime * TRACE_SPEED;
const textEndFrame = Math.round((textEndWorldX / TRACE_SPEED) * fps);
const isTextPhase = headX > textStartWorldX;
const isTextDone = frame >= textEndFrame - 3;
// ── Viewport: keeps scrolling, head drifts from 75% → right edge ──
let headScreenX: number;
let viewOffset: number;
if (headX <= textStartWorldX) {
viewOffset = Math.max(0, headX - headScreenDuringEcg);
headScreenX = headX - viewOffset;
} else if (headX >= textEndWorldX) {
// Lock viewport so text stays centered; dot keeps moving right
viewOffset = textEndWorldX - finalHeadScreenX;
headScreenX = headX - viewOffset;
} else {
const p = (headX - textStartWorldX) / (textEndWorldX - textStartWorldX);
headScreenX = headScreenDuringEcg + p * (finalHeadScreenX - headScreenDuringEcg);
viewOffset = headX - headScreenX;
}
// ── Y function (world space) ──
function getYAtX(worldX: number): number {
for (const beat of beats) {
const beatStartX = (beat.startFrame / fps) * TRACE_SPEED;
const beatEndX = beatStartX + beat.widthPx;
if (worldX >= beatStartX && worldX <= beatEndX) {
const progress = (worldX - beatStartX) / beat.widthPx;
const beatPoints = generateHeartbeatPoints(beat.amplitude);
const idx = Math.min(
Math.floor(progress * (beatPoints.length - 1)),
beatPoints.length - 1,
);
return baselineY - beatPoints[idx].y * ecgMaxDeflection;
}
}
for (const item of textLayout) {
if (worldX >= item.startX && worldX <= item.endX) {
const t = (worldX - item.startX) / (item.endX - item.startX);
const letterDef = LETTERS[item.char];
if (letterDef) {
return baselineY - interpolateLetterY(letterDef, t) * textMaxDeflection;
}
}
}
return baselineY;
}
// ── ECG trace path (up to text start) ──
const firstBeatWorldX = (beats[0].startFrame / fps) * TRACE_SPEED;
const traceStartWX = Math.max(Math.floor(firstBeatWorldX), Math.floor(viewOffset));
const ecgTraceEndWX = Math.min(
Math.ceil(headX),
Math.ceil(textStartWorldX),
Math.ceil(viewOffset + width),
);
const traceSegments: string[] = [];
if (ecgTraceEndWX >= traceStartWX) {
for (let wx = traceStartWX; wx <= ecgTraceEndWX; wx++) {
const sx = wx - viewOffset;
const y = getYAtX(wx);
traceSegments.push(wx === traceStartWX ? `M ${sx} ${y}` : `L ${sx} ${y}`);
}
}
const tracePathD = traceSegments.join(" ");
// ── Flat exit line after text finishes ──
let exitPathD = "";
if (isTextDone && headX > textEndWorldX) {
const exitStartSX = textEndWorldX - viewOffset - 32;
const exitEndSX = headX - viewOffset;
exitPathD = `M ${exitStartSX} ${baselineY} L ${exitEndSX} ${baselineY}`;
}
// ── Neon fade ──
const neonLengthPx = 200;
const neonFadeScreenEnd = headScreenX;
const neonFadeScreenStart = neonFadeScreenEnd - neonLengthPx;
// ── Text mask ──
const maskBrushSize = 1;
const clipLeadPx = 20;
const blockUnmaskDelay = 15;
const blockFeatherPx = 10;
const textMaskEndSX = isTextPhase
? (isTextDone ? width : Math.max(0, Math.min(Math.ceil(headScreenX), width)))
: 0;
const textMaskSegments: string[] = [];
if (isTextPhase && textMaskEndSX > 0 && !isTextDone) {
for (let sx = 0; sx <= textMaskEndSX; sx++) {
const y = getYAtX(viewOffset + sx);
textMaskSegments.push(sx === 0 ? `M ${sx} ${y}` : `L ${sx} ${y}`);
}
}
const textMaskPathD = textMaskSegments.join(" ");
const blockUnmaskX = isTextDone ? width : Math.max(0, textMaskEndSX - blockUnmaskDelay);
// ── Connectors (screen space) ──
const connectorSegments: string[] = [];
for (let i = 0; i < textLayout.length - 1; i++) {
const curr = textLayout[i];
const next = textLayout[i + 1];
connectorSegments.push(
`M ${curr.endConnector - viewOffset - 18} ${baselineY} L ${next.startConnector - viewOffset} ${baselineY}`,
);
}
const connectorPathD = connectorSegments.join(" ");
return (
<AbsoluteFill style={{ backgroundColor: "#000000", overflow: "hidden" }}>
<svg width={width} height={height} style={{ position: "absolute", top: 0, left: 0 }}>
<defs>
<filter id="neon" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur1" />
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur2" />
<feGaussianBlur in="SourceGraphic" stdDeviation="14" result="blur3" />
<feMerge>
<feMergeNode in="blur3" />
<feMergeNode in="blur2" />
<feMergeNode in="blur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="neonText" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="tblur1" />
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="tblur2" />
<feMerge>
<feMergeNode in="tblur2" />
<feMergeNode in="tblur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<linearGradient
id="neonMaskGrad"
gradientUnits="userSpaceOnUse"
x1={neonFadeScreenStart} y1={0}
x2={neonFadeScreenEnd} y2={0}
>
<stop offset="0%" stopColor="black" />
<stop offset="100%" stopColor="white" />
</linearGradient>
<mask id="neonMask">
<rect x={0} y={0} width={width} height={height} fill="url(#neonMaskGrad)" />
</mask>
<clipPath id="textReveal">
<rect
x={0} y={0}
width={isTextDone ? width : Math.max(0, headScreenX + clipLeadPx)}
height={height}
/>
</clipPath>
<linearGradient
id="blockUnmaskGrad"
gradientUnits="userSpaceOnUse"
x1={blockUnmaskX - blockFeatherPx} y1={0}
x2={blockUnmaskX} y2={0}
>
<stop offset="0%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</linearGradient>
<mask id="textWipeMask">
<rect x={0} y={0} width={width} height={height} fill={isTextDone ? "white" : "black"} />
{!isTextDone && blockUnmaskX > 0 && (
<rect x={0} y={0} width={blockUnmaskX} height={height} fill="url(#blockUnmaskGrad)" />
)}
{!isTextDone && textMaskPathD && (
<path
d={textMaskPathD}
fill="none"
stroke="white"
strokeWidth={15 * maskBrushSize}
strokeLinejoin="round"
strokeLinecap="round"
filter="url(#neonText)"
/>
)}
</mask>
<radialGradient id="headGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#ffffff" stopOpacity={0.8} />
<stop offset="30%" stopColor={lineColor} stopOpacity={0.6} />
<stop offset="100%" stopColor={lineColor} stopOpacity={0} />
</radialGradient>
</defs>
{/* ECG trace */}
{tracePathD && (
<g>
<path d={tracePathD} fill="none" stroke={lineColor} strokeWidth={2}
strokeLinejoin="round" strokeLinecap="round" />
<path d={tracePathD} fill="none" stroke={lineColor} strokeWidth={2.5}
strokeLinejoin="round" strokeLinecap="round"
filter="url(#neon)" mask="url(#neonMask)" />
</g>
)}
{/* Text + connectors */}
{isTextPhase && (
<g clipPath="url(#textReveal)">
<g mask="url(#textWipeMask)">
{textLayout.map((item, i) => (
<text
key={i}
x={(item.startX + item.endX) / 2 - viewOffset}
y={baselineY}
textAnchor="middle"
dominantBaseline="alphabetic"
fontSize={Math.round(textMaxDeflection / 0.715)}
fontFamily="Arial, Helvetica, sans-serif"
fontWeight="bold"
fill="none"
stroke={lineColor}
strokeWidth={1.5}
filter="url(#neonText)"
>
{item.char}
</text>
))}
{connectorPathD && (
<path d={connectorPathD} fill="none" stroke={lineColor}
strokeWidth={1.5} strokeLinecap="round" />
)}
</g>
</g>
)}
{/* Flat exit line after text */}
{exitPathD && (
<g>
<path d={exitPathD} fill="none" stroke={lineColor} strokeWidth={2}
strokeLinejoin="round" strokeLinecap="round" />
<path d={exitPathD} fill="none" stroke={lineColor} strokeWidth={2.5}
strokeLinejoin="round" strokeLinecap="round"
filter="url(#neon)" />
</g>
)}
{/* Head dot */}
{headScreenX >= 0 && headScreenX <= width && (
<>
<circle cx={headScreenX} cy={getYAtX(headX)} r={20} fill="url(#headGlow)" />
<circle cx={headScreenX} cy={getYAtX(headX)} r={3} fill={lineColor} />
</>
)}
</svg>
{/* Scanlines */}
<div style={{
position: "absolute", top: 0, left: 0, width: "100%", height: "100%",
background: "repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.08) 2px, rgba(0,0,0,0.08) 4px)",
pointerEvents: "none",
}} />
{/* Vignette */}
<div style={{
position: "absolute", top: 0, left: 0, width: "100%", height: "100%",
background: "radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.5) 100%)",
pointerEvents: "none",
}} />
</AbsoluteFill>
);
};
+11 -9
View File
@@ -24,25 +24,27 @@ Each task below references a specific file in `Ralph/refs/` — read ONLY that f
- [x] **Task 1: Design system foundation and font setup.** Read `Ralph/refs/ref-design-system.md`. Audit and fix the Tailwind config (`tailwind.config.js`) and global CSS (`src/index.css`) to ensure ALL PMR color tokens, typography, and spacing match the design system spec exactly. Specific fixes needed: (a) Ensure Geist Mono font is loaded via Google Fonts or local import — currently the project uses Fira Code for monospace but the spec requires Geist Mono for coded entries, timestamps, and data values. (b) Verify all PMR color tokens exist in Tailwind config: main content `#F5F7FA`, cards `#FFFFFF`, sidebar `#1E293B`, patient banner `#334155`, NHS blue `#005EB8`, green `#22C55E`, amber `#F59E0B`, red `#EF4444`, text primary `#111827`, text secondary `#6B7280`. (c) Ensure border-radius defaults to 4px for cards/inputs (not 8px or 12px — clinical systems use minimal rounding). (d) Add a `.pmr-theme` class or CSS custom properties layer for PMR-specific tokens if not already present. (e) Verify Inter font is loaded and configured as the primary font family. Do NOT invoke /frontend-design for this task — it's pure configuration. - [x] **Task 1: Design system foundation and font setup.** Read `Ralph/refs/ref-design-system.md`. Audit and fix the Tailwind config (`tailwind.config.js`) and global CSS (`src/index.css`) to ensure ALL PMR color tokens, typography, and spacing match the design system spec exactly. Specific fixes needed: (a) Ensure Geist Mono font is loaded via Google Fonts or local import — currently the project uses Fira Code for monospace but the spec requires Geist Mono for coded entries, timestamps, and data values. (b) Verify all PMR color tokens exist in Tailwind config: main content `#F5F7FA`, cards `#FFFFFF`, sidebar `#1E293B`, patient banner `#334155`, NHS blue `#005EB8`, green `#22C55E`, amber `#F59E0B`, red `#EF4444`, text primary `#111827`, text secondary `#6B7280`. (c) Ensure border-radius defaults to 4px for cards/inputs (not 8px or 12px — clinical systems use minimal rounding). (d) Add a `.pmr-theme` class or CSS custom properties layer for PMR-specific tokens if not already present. (e) Verify Inter font is loaded and configured as the primary font family. Do NOT invoke /frontend-design for this task — it's pure configuration.
- [ ] **Task 2: Rebuild LoginScreen component.** Read `Ralph/refs/ref-transition-login.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/LoginScreen.tsx` to match the login sequence specification exactly: (a) Dark blue-gray `#1E293B` background. (b) White card: 320px wide, **12px border-radius** (exception to the 4px rule — login cards can be rounder), subtle shadow. (c) NHS-blue shield icon at top with "CareerRecord PMR" branding text. (d) Username field types `A.CHARLWOOD` at 30ms/char in **Geist Mono** font. (e) Password field fills 8 dots at 20ms/dot. (f) Blinking cursor (530ms interval) in active field. (g) "Log In" button: NHS blue `#005EB8`, full width, pressed state darkens to `#004494`. (h) After submit: card scales to 103% and fades out over 200ms. (i) Respect `prefers-reduced-motion`. The login must feel like actually logging into NHS software at 8am on a Monday. - [ ] **Task 1b: Rebuild boot sequence and ECG animation.** Read `Ralph/refs/ref-boot-ecg.md` and `Ralph/refs/ref-design-system.md`. Also read `ECGCombined.tsx` in the project root for the Remotion reference implementation of the mask-based text reveal. This task covers the full pre-login animation flow: (a) **Refactor BootSequence.tsx** — replace hardcoded HTML strings with a clean config-driven structure. Each line type (header, field, separator, module, ready) maps to a React component. Keep the same visual output: green-on-black terminal, Fira Code font, 220ms staggered line reveals, `#00ff41` bright green / `#3a6b45` dim green / `#00e5ff` cyan labels. (b) **Cursor → dot transition** — the blinking green cursor at the end of boot must smoothly morph into the ECG's glowing trace dot. Capture the cursor's screen position and pass it to ECGAnimation as a `startPosition` prop. The cursor stops blinking, transitions from block to circular glow (~300ms), then begins moving rightward as the ECG trace dot. (c) **ECG start sync** — ECGAnimation must start its trace from the cursor position (not the far left edge). The first beat begins after a flat gap from the cursor position. Shift the world-space origin so the trace starts where the cursor was. (d) **Mask-based text reveal** — adopt ECGCombined.tsx's technique where pre-rendered stroke-only text is revealed by a wipe mask following the trace head (instead of the current alpha fade approach). Keep the current character spacing (`LETTER_W`, `LETTER_G`, `SPACE_W`) and heartbeat waveform. Add connector lines between letters at baseline. (e) **Keep**: heartbeat shape, beat timing (0.3→0.55→0.85→1.0 amplitude), canvas rendering, viewport scrolling, flatline draw, scanlines, vignette, background transition to `#1E293B`. (f) Respect `prefers-reduced-motion` — with reduced motion, skip animation and show static final frame or jump to login.
- [ ] **Task 3: Rebuild PatientBanner component.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/PatientBanner.tsx` to match the specification exactly: (a) Full banner 80px: background `#334155`, bottom border `1px solid #475569`. Name in Inter 600 **20px** (not 18px), details in Inter 400 14px. Layout must match the ASCII art in the ref file — surname-first format "CHARLWOOD, Andrew (Mr)", DOB/NHS No/Address on second row, phone/email/buttons on third row. (b) Status: green dot + "Active" text. Badge: "Open to opportunities" as blue pill. (c) Action buttons: outlined rectangles with NHS blue text and 1px border, 4px radius. Hover fills with NHS blue bg + white text. (d) Condensed banner 48px: single line with name, NHS number, status, action buttons only. Triggers at 100px scroll via IntersectionObserver. Smooth 200ms height transition. (e) Mobile banner: minimal top bar `CHARLWOOD, A (Mr) | 2211810 | dot` with overflow "..." menu. NHS Number tooltip: "GPhC Registration Number". - [ ] **Task 2: Rebuild LoginScreen component.** Read `Ralph/refs/ref-transition-login.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/LoginScreen.tsx` to match the login sequence specification exactly: (a) Dark blue-gray `#1E293B` background. (b) White card: 320px wide, **12px border-radius** (exception to the 4px rule — login cards can be rounder), subtle shadow. (c) NHS-blue shield icon at top with "CareerRecord PMR" branding text. (d) Username field types `A.CHARLWOOD` at 30ms/char in **Geist Mono** font. (e) Password field fills 8 dots at 20ms/dot. (f) Blinking cursor (530ms interval) in active field. (g) "Log In" button: NHS blue `#005EB8`, full width, pressed state darkens to `#004494`. (h) After submit: card scales to 103% and fades out over 200ms. (i) Respect `prefers-reduced-motion`. The login must feel like actually logging into NHS software at 8am on a Monday.
- [ ] **Task 4: Rebuild ClinicalSidebar component.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/ClinicalSidebar.tsx` to match the specification exactly: (a) 220px fixed width, `#1E293B` background, `1px solid #334155` right border. (b) Header: "CareerRecord PMR / v1.0.0" in Inter 500, 13px, white at 50% opacity. (c) Search input: "Search record..." placeholder, integrated in sidebar below header. (d) **Navigation labels use CV-friendly terms**: Summary, Experience, Skills, Achievements, Projects, Education, Contact (NOT clinical jargon like Consultations, Medications, etc.). Same Lucide icons as before. Items: 44px height, 16px left padding, icons 18px + labels Inter 500 14px. Exact states: default white/70%, hover white/100% + `rgba(255,255,255,0.08)` bg, active white/100% + 3px NHS blue left border + `rgba(255,255,255,0.12)` bg + Inter 600 bold. (e) Separator line between Summary and Experience. (f) Footer: "Session: A.CHARLWOOD / Logged in: [time]" in Inter 400, 11px, `#64748B`. Time updates on mount. (g) Tablet mode: 56px icon-only with tooltips. (h) URL hash routing (`#summary`, `#experience`, `#skills`, `#achievements`, `#projects`, `#education`, `#contact`). (i) Alt+1-7 keyboard shortcuts, arrow key navigation, "/" for search focus. (j) Update the ViewId type in `src/types/pmr.ts` if needed to match the new labels. - [ ] **Task 3: Rebuild PatientBanner component.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/PatientBanner.tsx` to match the specification exactly: (a) Full banner 80px: background `#334155`, bottom border `1px solid #475569`. Name in Inter 600 **20px** (not 18px), details in Inter 400 14px. Layout must match the ASCII art in the ref file — surname-first format "CHARLWOOD, Andrew (Mr)", DOB/NHS No/Address on second row, phone/email/buttons on third row. (b) Status: green dot + "Active" text. Badge: "Open to opportunities" as blue pill. (c) Action buttons: outlined rectangles with NHS blue text and 1px border, 4px radius. Hover fills with NHS blue bg + white text. (d) Condensed banner 48px: single line with name, NHS number, status, action buttons only. Triggers at 100px scroll via IntersectionObserver. Smooth 200ms height transition. (e) Mobile banner: minimal top bar `CHARLWOOD, A (Mr) | 2211810 | dot` with overflow "..." menu. NHS Number tooltip: "GPhC Registration Number".
- [ ] **Task 4: Rebuild ClinicalSidebar component.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/ClinicalSidebar.tsx` to match the specification exactly: (a) 220px fixed width, `#1E293B` background, `1px solid #334155` right border. (b) Header: "CareerRecord PMR / v1.0.0" in Inter 500, 13px, white at 50% opacity. (c) Search input: "Search record..." placeholder, integrated in sidebar below header. (d) **Navigation labels use CV-friendly terms**: Summary, Experience, Skills, Achievements, Projects, Education, Contact (NOT clinical jargon like Consultations, Medications, etc.). Same Lucide icons as before. Items: 44px height, 16px left padding, icons 18px + labels Inter 500 14px. Exact states: default white/70%, hover white/100% + `rgba(255,255,255,0.08)` bg, active white/100% + 3px NHS blue left border + `rgba(255,255,255,0.12)` bg + Inter 600 bold. (e) Separator line between Summary and Experience. (f) Footer: "Session: A.CHARLWOOD / Logged in: [time]" in Inter 400, 11px, `#64748B`. Time updates on mount. (g) Tablet mode: 56px icon-only with tooltips. (h) URL hash routing (`#summary`, `#experience`, `#skills`, `#achievements`, `#projects`, `#education`, `#contact`). (i) Alt+1-7 keyboard shortcuts, arrow key navigation, "/" for search focus. (j) Update the ViewId type in `src/types/pmr.ts` if needed to match the new labels.
- [ ] **Task 5: Rebuild PMRInterface layout and add Breadcrumb.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/PMRInterface.tsx` to ensure correct layout composition: (a) Fixed sidebar (220px) + sticky patient banner + scrollable main content area with `#F5F7FA` background. (b) Main content padding: 24px. No max-width — content fills available space. (c) Create `src/components/Breadcrumb.tsx`: displays "Patient Record > [Current View]" at the top of the main content area (using CV-friendly view names: Experience, Skills, Achievements, etc.). When a consultation/skill/achievement is expanded, the breadcrumb deepens to show the item name. Styled in Inter 400, 13px, gray-400, with chevron separators. Clickable links navigate back. (d) Interface materialization animations: patient banner slides down (200ms ease-out), sidebar slides from left (250ms ease-out, 50ms delay), content fades in (300ms, 100ms delay after sidebar). (e) View switching must be INSTANT — no crossfade or slide between views. (f) Ensure mobile layout uses bottom nav bar (56px height with safe area padding). (g) Update the view switching logic to use the new CV-friendly ViewId values (summary, experience, skills, achievements, projects, education, contact). - [ ] **Task 5: Rebuild PMRInterface layout and add Breadcrumb.** Read `Ralph/refs/ref-banner-sidebar.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/PMRInterface.tsx` to ensure correct layout composition: (a) Fixed sidebar (220px) + sticky patient banner + scrollable main content area with `#F5F7FA` background. (b) Main content padding: 24px. No max-width — content fills available space. (c) Create `src/components/Breadcrumb.tsx`: displays "Patient Record > [Current View]" at the top of the main content area (using CV-friendly view names: Experience, Skills, Achievements, etc.). When a consultation/skill/achievement is expanded, the breadcrumb deepens to show the item name. Styled in Inter 400, 13px, gray-400, with chevron separators. Clickable links navigate back. (d) Interface materialization animations: patient banner slides down (200ms ease-out), sidebar slides from left (250ms ease-out, 50ms delay), content fades in (300ms, 100ms delay after sidebar). (e) View switching must be INSTANT — no crossfade or slide between views. (f) Ensure mobile layout uses bottom nav bar (56px height with safe area padding). (g) Update the view switching logic to use the new CV-friendly ViewId values (summary, experience, skills, achievements, projects, education, contact).
- [ ] **Task 6: Rebuild SummaryView with Clinical Alert.** Read `Ralph/refs/ref-summary-alert.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/views/SummaryView.tsx` and the clinical alert: (a) Clinical Alert: amber `#FEF3C7` background, 4px left border `#F59E0B`, text `#92400E`. Slides down with **spring animation** (not ease-out — use Framer Motion `type: "spring"` with appropriate damping). Acknowledge button: amber outlined. On click: warning icon cross-fades to green checkmark (200ms), holds 200ms, then alert height collapses (200ms ease-out). (b) 2-column card grid on desktop, single column on mobile. Cards have: header bar with title in Inter 600, 14px, uppercase, on `#F9FAFB` background with `1px solid #E5E7EB` bottom border. Card body: 16px padding, `1px solid #E5E7EB` border, 4px radius. (c) Demographics card: two-column key-value layout. Labels right-aligned Inter 500 13px gray-500, values left-aligned Inter 400 14px gray-900. (d) Active Problems card: traffic light dots + problem text + date in Geist Mono 12px. (e) Quick Meds card: 4-column table with top 5 skills. "View Full List ->" link. (f) Last Consultation card: preview with "View Full Record ->" link. All navigation links must actually switch the sidebar view. - [ ] **Task 6: Rebuild SummaryView with Clinical Alert.** Read `Ralph/refs/ref-summary-alert.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/views/SummaryView.tsx` and the clinical alert: (a) Clinical Alert: amber `#FEF3C7` background, 4px left border `#F59E0B`, text `#92400E`. Slides down with **spring animation** (not ease-out — use Framer Motion `type: "spring"` with appropriate damping). Acknowledge button: amber outlined. On click: warning icon cross-fades to green checkmark (200ms), holds 200ms, then alert height collapses (200ms ease-out). (b) 2-column card grid on desktop, single column on mobile. Cards have: header bar with title in Inter 600, 14px, uppercase, on `#F9FAFB` background with `1px solid #E5E7EB` bottom border. Card body: 16px padding, `1px solid #E5E7EB` border, 4px radius. (c) Demographics card: two-column key-value layout. Labels right-aligned Inter 500 13px gray-500, values left-aligned Inter 400 14px gray-900. (d) Active Problems card: traffic light dots + problem text + date in Geist Mono 12px. (e) Quick Meds card: 4-column table with top 5 skills. "View Full List ->" link. (f) Last Consultation card: preview with "View Full Record ->" link. All navigation links must actually switch the sidebar view.
- [ ] **Task 7: Rebuild ConsultationsView.** Read `Ralph/refs/ref-consultations.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/views/ConsultationsView.tsx`: (a) Collapsed entries: date in Geist Mono 13px gray-500, organization in Inter 400 13px NHS blue, role in Inter 600 15px gray-900, "Key:" prefix in Inter 500 gray-500 with single-line achievement. Chevron button right-aligned. (b) Status dot: green for current, gray for historical. (c) 3px left border color-coded by employer (NHS blue `#005EB8` or Tesco teal `#00897B`). (d) Expanded state: Duration line, then HISTORY / EXAMINATION / PLAN sections with headers in Inter 600, 12px, uppercase, letter-spacing 0.05em, gray-400. Plan items as bullet lists. (e) CODED ENTRIES section at bottom: Geist Mono 12px, gray-500, bracket codes. (f) Height-only expand animation, 200ms ease-out. NO opacity fade on content — content simply grows/shrinks. (g) Only one expanded at a time. (h) Implement the second clinical alert (Python switching algorithm alert) that appears on first navigation to Consultations view, dismissible with same Acknowledge pattern. - [ ] **Task 7: Rebuild ConsultationsView.** Read `Ralph/refs/ref-consultations.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/views/ConsultationsView.tsx`: (a) Collapsed entries: date in Geist Mono 13px gray-500, organization in Inter 400 13px NHS blue, role in Inter 600 15px gray-900, "Key:" prefix in Inter 500 gray-500 with single-line achievement. Chevron button right-aligned. (b) Status dot: green for current, gray for historical. (c) 3px left border color-coded by employer (NHS blue `#005EB8` or Tesco teal `#00897B`). (d) Expanded state: Duration line, then HISTORY / EXAMINATION / PLAN sections with headers in Inter 600, 12px, uppercase, letter-spacing 0.05em, gray-400. Plan items as bullet lists. (e) CODED ENTRIES section at bottom: Geist Mono 12px, gray-500, bracket codes. (f) Height-only expand animation, 200ms ease-out. NO opacity fade on content — content simply grows/shrinks. (g) Only one expanded at a time. (h) Implement the second clinical alert (Python switching algorithm alert) that appears on first navigation to Consultations view, dismissible with same Acknowledge pattern.
- [ ] **Task 8: Rebuild MedicationsView.** Read `Ralph/refs/ref-medications.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/views/MedicationsView.tsx`: (a) Three category tabs: "Active Medications" (technical), "Clinical Medications" (healthcare domain), "PRN (As Required)" (strategic/leadership). Tab styling: active tab has white bg + NHS blue bottom border. (b) Full table with proper `<table>` markup: Drug Name | Dose | Frequency | Start | Status. Table headers: Inter 600, 13px, uppercase, `#F9FAFB` bg. Row height: 40px. All borders: `1px solid #E5E7EB`. Alternating `#FFFFFF`/`#F9FAFB` row backgrounds. (c) Hover: `#EFF6FF` background only — no transform, no lift. (d) Sortable columns: click header to sort, arrow indicator in active column. (e) Status dots: 6px green circles + "Active" text. (f) Expandable prescribing history: Geist Mono 12px, year markers bold, descriptions regular. (g) Mobile: card layout (stacked fields per card, not table). - [ ] **Task 8: Rebuild MedicationsView.** Read `Ralph/refs/ref-medications.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/views/MedicationsView.tsx`: (a) Three category tabs: "Active Medications" (technical), "Clinical Medications" (healthcare domain), "PRN (As Required)" (strategic/leadership). Tab styling: active tab has white bg + NHS blue bottom border. (b) Full table with proper `<table>` markup: Drug Name | Dose | Frequency | Start | Status. Table headers: Inter 600, 13px, uppercase, `#F9FAFB` bg. Row height: 40px. All borders: `1px solid #E5E7EB`. Alternating `#FFFFFF`/`#F9FAFB` row backgrounds. (c) Hover: `#EFF6FF` background only — no transform, no lift. (d) Sortable columns: click header to sort, arrow indicator in active column. (e) Status dots: 6px green circles + "Active" text. (f) Expandable prescribing history: Geist Mono 12px, year markers bold, descriptions regular. (g) Mobile: card layout (stacked fields per card, not table).
- [ ] **Task 9: Rebuild ProblemsView.** Read `Ralph/refs/ref-problems.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/views/ProblemsView.tsx`: (a) Two sections: "ACTIVE PROBLEMS" and "RESOLVED PROBLEMS" with section headers in clinical style. (b) Table columns — Active: Status | Code | Problem | Since. Resolved: Status | Code | Problem | Resolved | Outcome. (c) Traffic light dots: 8px circles (green=resolved, amber=in-progress). Always paired with text labels — never the sole indicator. (d) Code column in Geist Mono. (e) Expandable rows showing full narrative + linked consultation buttons that navigate to Consultations view. (f) Row hover: `#EFF6FF`. (g) Mobile: card layout. - [ ] **Task 9: Rebuild ProblemsView.** Read `Ralph/refs/ref-problems.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/views/ProblemsView.tsx`: (a) Two sections: "ACTIVE PROBLEMS" and "RESOLVED PROBLEMS" with section headers in clinical style. (b) Table columns — Active: Status | Code | Problem | Since. Resolved: Status | Code | Problem | Resolved | Outcome. (c) Traffic light dots: 8px circles (green=resolved, amber=in-progress). Always paired with text labels — never the sole indicator. (d) Code column in Geist Mono. (e) Expandable rows showing full narrative + linked consultation buttons that navigate to Consultations view. (f) Row hover: `#EFF6FF`. (g) Mobile: card layout.
- [ ] **Task 10: Rebuild InvestigationsView and DocumentsView.** Read `Ralph/refs/ref-investigations-documents.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild both views: (a) InvestigationsView: table with Test Name | Requested | Status | Result. Status badges: green "Complete", amber "Ongoing", pulsing green "Live". Expanded view uses tree-indented monospace structure with box-drawing characters (pipe symbols). "View Results" NHS blue button for PharMetrics only. (b) DocumentsView: table with Type icon | Document | Date | Source. Lucide icons per document type (FileText, Award, GraduationCap, FlaskConical). Expanded preview uses same tree-indented structure. (c) Both views share the expandable-row pattern — maintain visual consistency. (d) Mobile: card layouts for both. - [ ] **Task 10: Rebuild InvestigationsView and DocumentsView.** Read `Ralph/refs/ref-investigations-documents.md` and `Ralph/refs/ref-design-system.md`. Rebuild both views: (a) InvestigationsView: table with Test Name | Requested | Status | Result. Status badges: green "Complete", amber "Ongoing", pulsing green "Live". Expanded view uses tree-indented monospace structure with box-drawing characters (pipe symbols). "View Results" NHS blue button for PharMetrics only. (b) DocumentsView: table with Type icon | Document | Date | Source. Lucide icons per document type (FileText, Award, GraduationCap, FlaskConical). Expanded preview uses same tree-indented structure. (c) Both views share the expandable-row pattern — maintain visual consistency. (d) Mobile: card layouts for both.
- [ ] **Task 11: Rebuild ReferralsView.** Read `Ralph/refs/ref-referrals.md` and `Ralph/refs/ref-design-system.md`. Invoke `/frontend-design` skill BEFORE writing code. Rebuild `src/components/views/ReferralsView.tsx`: (a) Form header: "New Referral" with pre-filled patient info (non-editable). (b) Priority radio buttons: Urgent (red), Routine (blue, default), Two-Week Wait (amber). Each with tongue-in-cheek tooltips. (c) Form fields: standard clinical inputs with `1px solid #D1D5DB` border, 4px radius, `8px 12px` padding. Labels in Inter 500, 13px, gray-600 above inputs. Focus: NHS blue border + `box-shadow: 0 0 0 3px rgba(0,94,184,0.15)`. (d) Contact Method radio group. (e) Submit: "Send Referral" NHS blue button. Loading spinner state. Success state with reference number (REF-YYYY-MMDD-NNN format), checkmark, "24-48 hours" response time. (f) Direct Contact table below form: Email, Phone, LinkedIn, Location with action buttons. - [ ] **Task 11: Rebuild ReferralsView.** Read `Ralph/refs/ref-referrals.md` and `Ralph/refs/ref-design-system.md`. Rebuild `src/components/views/ReferralsView.tsx`: (a) Form header: "New Referral" with pre-filled patient info (non-editable). (b) Priority radio buttons: Urgent (red), Routine (blue, default), Two-Week Wait (amber). Each with tongue-in-cheek tooltips. (c) Form fields: standard clinical inputs with `1px solid #D1D5DB` border, 4px radius, `8px 12px` padding. Labels in Inter 500, 13px, gray-600 above inputs. Focus: NHS blue border + `box-shadow: 0 0 0 3px rgba(0,94,184,0.15)`. (d) Contact Method radio group. (e) Submit: "Send Referral" NHS blue button. Loading spinner state. Success state with reference number (REF-YYYY-MMDD-NNN format), checkmark, "24-48 hours" response time. (f) Direct Contact table below form: Email, Phone, LinkedIn, Location with action buttons.
- [ ] **Task 12: Implement fuzzy search with fuse.js.** Read `Ralph/refs/ref-interactions.md`. Install fuse.js (`npm install fuse.js`). Rebuild the search functionality in the sidebar: (a) Build a search index on mount from ALL content: consultation titles/descriptions, medication names, problem descriptions, investigation names, document titles. (b) Search options: `{ keys: ['title', 'description', 'name'], threshold: 0.3, includeScore: true }`. (c) Results dropdown grouped by section (Consultations, Medications, Problems, etc.) with section icon, matching text, and relevance indicator. (d) Clicking a result navigates to the section AND expands/highlights the matching item. (e) Dropdown styled: white bg, `1px solid #E5E7EB`, 4px radius, shadow. Items in Inter 400 14px, 36px height. (f) Escape closes dropdown. (g) Mobile: search bar at top of each view instead of sidebar. - [ ] **Task 12: Implement fuzzy search with fuse.js.** Read `Ralph/refs/ref-interactions.md`. Install fuse.js (`npm install fuse.js`). Rebuild the search functionality in the sidebar: (a) Build a search index on mount from ALL content: consultation titles/descriptions, medication names, problem descriptions, investigation names, document titles. (b) Search options: `{ keys: ['title', 'description', 'name'], threshold: 0.3, includeScore: true }`. (c) Results dropdown grouped by section (Consultations, Medications, Problems, etc.) with section icon, matching text, and relevance indicator. (d) Clicking a result navigates to the section AND expands/highlights the matching item. (e) Dropdown styled: white bg, `1px solid #E5E7EB`, 4px radius, shadow. Items in Inter 400 14px, 36px height. (f) Escape closes dropdown. (g) Mobile: search bar at top of each view instead of sidebar.
+78 -16
View File
@@ -2,14 +2,24 @@
You are operating inside an automated loop. Each iteration you receive fresh context - you have NO memory of previous iterations. Your only persistence is the filesystem. You are operating inside an automated loop. Each iteration you receive fresh context - you have NO memory of previous iterations. Your only persistence is the filesystem.
You are implementing **Design 7: The Clinical Record** — a Patient Medical Record (PMR) system that presents Andy's CV as a clinician would view a patient record. This is a complete redesign from the previous ECG Heartbeat concept. You are implementing **Design 7: The Clinical Record** — a Patient Medical Record (PMR) system that presents Andy's CV as a clinician would view a patient record. This is a visual redesign rebuilding existing components to achieve absolute thematic fidelity to real NHS clinical software.
**The Concept:** **The Concept:**
The "patient" is Andy's career. Users navigate a genuine NHS clinical software interface (similar to EMIS Web, SystmOne, Vision) with a patient banner, sidebar navigation, consultation journal, medications table, clinical alerts, and a login sequence. The design works on two levels: clinicians recognize the interface immediately; recruiters get a novel, information-dense presentation. The "patient" is Andy's career. Users navigate a genuine NHS clinical software interface (similar to EMIS Web, SystmOne, Vision) with a patient banner, sidebar navigation, consultation journal, medications table, clinical alerts, and a login sequence. The clinical metaphor lives in the LAYOUT and VISUAL PRESENTATION — the sidebar labels use CV-friendly terms (Experience, Skills, Achievements, Projects, Education, Contact) while each view is laid out like its clinical equivalent (consultation journal, medications table, problems list, etc.).
**IMPORTANT — Sidebar Label Convention:**
The sidebar uses CV-intuitive labels, NOT clinical jargon. But each view's content is presented in the clinical format:
- **Summary** → Patient summary layout
- **Experience** (not "Consultations") → Consultation journal layout with History/Examination/Plan
- **Skills** (not "Medications") → Medications table layout with dosages/frequency
- **Achievements** (not "Problems") → Problems list layout with traffic lights
- **Projects** (not "Investigations") → Investigation results layout
- **Education** (not "Documents") → Attached documents layout
- **Contact** (not "Referrals") → Referral form layout
## Your Task This Iteration ## Your Task This Iteration
1. **Use the /frontend-design skill** (REQUIRED for visual components): Before writing ANY code for components that involve visual design, styling, animations, or UI elements, you MUST invoke the `/frontend-design` skill. This includes: LoginScreen, PatientBanner, ClinicalSidebar, ClinicalAlert, all View components (Summary, Consultations, Medications, Problems, Investigations, Documents, Referrals), and any table, card, or form component. 1. **Read the Design Guidance in the reference file** (REQUIRED for visual components): Each reference file in `Ralph/refs/` contains a "Design Guidance (from /frontend-design)" section at the bottom with pre-generated design direction, code patterns, and implementation details. You MUST read this section before writing code — it provides the aesthetic direction and code examples for the component. Do NOT invoke the `/frontend-design` skill at runtime — the guidance is already embedded in the ref files.
2. **Read the plan**: Open `IMPLEMENTATION_PLAN.md` and find the highest-priority unchecked item (`- [ ]`). Items are listed in priority order - pick the first unchecked one. 2. **Read the plan**: Open `IMPLEMENTATION_PLAN.md` and find the highest-priority unchecked item (`- [ ]`). Items are listed in priority order - pick the first unchecked one.
@@ -25,24 +35,61 @@ The "patient" is Andy's career. Users navigate a genuine NHS clinical software i
- Login typing animation specifics - Login typing animation specifics
- Consultation History/Examination/Plan format - Consultation History/Examination/Plan format
- Coded entries in [XXX000] format - Coded entries in [XXX000] format
- Sidebar labels are CV-friendly (Experience, Skills, etc.), NOT clinical jargon
5. **Implement the item**: Complete the single task you selected. Keep changes focused - one task per iteration. Write production-quality React/TypeScript code that faithfully reproduces a clinical information system. This is a design showcase requiring absolute thematic fidelity. 5. **Implement the item**: Complete the single task you selected. Keep changes focused - one task per iteration. Write production-quality React/TypeScript code that faithfully reproduces a clinical information system. This is a design showcase requiring absolute thematic fidelity.
6. **Run quality checks**: Execute the quality check commands listed in `IMPLEMENTATION_PLAN.md` under "Quality Checks". Fix any issues before proceeding. 6. **Run quality checks**: Execute the quality check commands listed in `IMPLEMENTATION_PLAN.md` under "Quality Checks". Fix any issues before proceeding.
7. **Commit your changes**: Stage and commit all changes with a descriptive message referencing the task you completed. 7. **Visual Review** (Tasks 1b-11 only — skip for non-visual tasks like Task 1, 12-15): After quality checks pass, verify your work visually in the browser using the Claude in Chrome browser tools:
a. Call `tabs_context_mcp` to get available tabs (create if empty).
b. Navigate to `http://localhost:5173` (dev server runs throughout the loop).
c. **First load only**: The app plays a boot→ECG→login→PMR sequence (~15s). Use `computer` with `action: "wait", duration: 15` then take a screenshot. On subsequent navigations in the same tab, the app stays in PMR phase — no waiting needed.
d. Navigate to the hash route for your task's view:
- Task 1b (Boot/ECG): Refresh page, screenshot during boot sequence, then again during ECG animation
- Task 2 (Login): Refresh page, wait ~8s (after boot+ECG), screenshot the login screen
- Task 3 (Banner): Any PMR view — review the patient banner at top
- Task 4 (Sidebar): Any PMR view — review left sidebar
- Task 5 (Layout/Breadcrumb): Any PMR view — review overall composition
- Task 6: `#summary` | Task 7: `#experience` | Task 8: `#skills`
- Task 9: `#achievements` | Task 10: `#projects` then `#education` | Task 11: `#contact`
e. Take a screenshot (`computer` with `action: "screenshot"`) and compare against your reference file.
f. Check specifically: colors match spec, correct font (Inter vs Geist Mono), proper spacing, `1px solid #E5E7EB` borders, 4px border-radius, layout alignment, NHS blue `#005EB8`.
g. If discrepancies are found: fix them, re-run quality checks, take another screenshot to confirm.
h. Note the visual review outcome in your progress.txt entry (step 10).
8. **Mark the item complete**: In `IMPLEMENTATION_PLAN.md`, change the item from `- [ ]` to `- [x]`. 8. **Commit your changes**: Stage and commit all changes with a descriptive message referencing the task you completed.
9. **Update progress.txt**: Append to the "Iteration Log" section with: 9. **Mark the item complete**: In `IMPLEMENTATION_PLAN.md`, change the item from `- [ ]` to `- [x]`.
10. **Update progress.txt**: Append to the "Iteration Log" section with:
- Which task you completed - Which task you completed
- Any learnings or codebase patterns discovered (add to "Codebase Patterns" section) - Any learnings or codebase patterns discovered (add to "Codebase Patterns" section)
- Any issues encountered - Any issues encountered
- Design decisions made (if visual component) - Design decisions made (if visual component)
- Visual review outcome (what was checked, any fixes made)
10. **Commit the progress update**: Stage and commit the updated `IMPLEMENTATION_PLAN.md` and `progress.txt`. 11. **Commit the progress update**: Stage and commit the updated `IMPLEMENTATION_PLAN.md` and `progress.txt`.
11. **Determine if another iteration is needed**: Review your work and the codebase. The project needs another iteration if ANY of these are true: 12. **Recommend model for next iteration**: Look at the NEXT unchecked task in `IMPLEMENTATION_PLAN.md` (the one after the task you just completed). Assess its complexity and output a model recommendation on its own line:
```
<next-model>sonnet</next-model>
```
or
```
<next-model>opus</next-model>
```
**Use this decision framework:**
- **Use `sonnet`** for: configuration tasks, search/utility implementation, responsive fixes, accessibility audits, tasks with very prescriptive specs, tasks that are mostly wiring/plumbing
- **Use `opus`** for: visual component rebuilds that invoke /frontend-design (design quality matters), complex animation work, tasks requiring strong aesthetic judgment, tasks where the previous iteration left issues that need creative problem-solving
- **Default to `sonnet`** if unsure — it's cheaper and handles well-specified tasks fine
- If there IS no next task (you just completed the last one), skip this step
13. **Determine if another iteration is needed**: Review your work and the codebase. The project needs another iteration if ANY of these are true:
- Any task in the checklist is unchecked (`- [ ]`) or blocked (`- [B]`) - Any task in the checklist is unchecked (`- [ ]`) or blocked (`- [B]`)
- Quality checks would fail (run them to verify) - Quality checks would fail (run them to verify)
- There are uncommitted changes - There are uncommitted changes
@@ -50,17 +97,19 @@ The "patient" is Andy's career. Users navigate a genuine NHS clinical software i
- The implementation doesn't fully satisfy the plan requirements - The implementation doesn't fully satisfy the plan requirements
- You have lingering doubts about correctness or completeness - You have lingering doubts about correctness or completeness
12. **Send completion signal ONLY if truly complete**: If and ONLY if the project definitely does NOT need another iteration — all tasks verified done, quality checks pass, no guidance for next iteration — output this exact signal on its own line: 14. **Send completion signal ONLY if truly complete**: If and ONLY if the project definitely does NOT need another iteration — all tasks verified done, quality checks pass, no guidance for next iteration — output this exact signal on its own line:
``` ```
<promise>COMPLETE</promise> <promise>COMPLETE</promise>
``` ```
DO NOT output this string if there's any chance another iteration is needed. When in doubt, do NOT send the promise — leave it for the next iteration to determine. DO NOT output this string if there's any chance another iteration is needed. When in doubt, do NOT send the promise — leave it for the next iteration to determine.
## Critical Rules ## Critical Rules
- **ALWAYS invoke /frontend-design skill before writing visual component code** — this is mandatory for all UI components - **ALWAYS read the "Design Guidance" section in the ref file before writing visual component code** — do NOT invoke /frontend-design at runtime (it's pre-baked into the ref files)
- **Do NOT invoke the /frontend-design skill** — the design guidance is already embedded in each ref file. Invoking it at runtime will consume your context and stall the iteration.
- **ALWAYS visually review visual components (Tasks 1b-11) in the browser** — use Claude in Chrome tools to screenshot and verify against the spec before committing
- **Only work on ONE task per iteration** - **Only work on ONE task per iteration**
- **Always read progress.txt AND guardrails.md before starting** — previous iterations may have left important context - **Always read progress.txt AND guardrails.md before starting** — previous iterations may have left important context
- **If a task is blocked or unclear**, document why in progress.txt and move to the next unchecked item - **If a task is blocked or unclear**, document why in progress.txt and move to the next unchecked item
@@ -68,15 +117,28 @@ DO NOT output this string if there's any chance another iteration is needed. Whe
- **If quality checks fail, fix the issues before committing** - **If quality checks fail, fix the issues before committing**
- **The visual quality bar is HIGH** — this must look like real clinical software - **The visual quality bar is HIGH** — this must look like real clinical software
- **Preserve clinical system authenticity** — instant navigation, proper tables, NHS blue, coded entries, traffic lights - **Preserve clinical system authenticity** — instant navigation, proper tables, NHS blue, coded entries, traffic lights
- **Sidebar labels are CV-friendly** — Experience (not Consultations), Skills (not Medications), etc.
- **Use TypeScript strictly** — no `any` types, proper interfaces for all PMR data structures - **Use TypeScript strictly** — no `any` types, proper interfaces for all PMR data structures
- **Follow the established project structure** — components in `src/components/`, data in `src/data/`, types in `src/types/` - **Follow the established project structure** — components in `src/components/`, data in `src/data/`, types in `src/types/`
- **Respect prefers-reduced-motion** — animations must have instant fallbacks - **Respect prefers-reduced-motion** — animations must have instant fallbacks
## Reference Files ## Reference Files
- `designs/07-the-clinical-record.md` — Complete design specification with all visual details, animations, and interactions Each task in the implementation plan references specific files in `Ralph/refs/`:
- `Ralph/refs/ref-boot-ecg.md` — Boot sequence + ECG animation improvements
- `Ralph/refs/ref-design-system.md` — Colors, typography, spacing, borders, motion
- `Ralph/refs/ref-transition-login.md` — ECG flatline + login sequence
- `Ralph/refs/ref-banner-sidebar.md` — Patient banner + sidebar + navigation
- `Ralph/refs/ref-summary-alert.md` — Summary view + clinical alert
- `Ralph/refs/ref-consultations.md` — Experience view (consultation journal layout)
- `Ralph/refs/ref-medications.md` — Skills view (medications table layout)
- `Ralph/refs/ref-problems.md` — Achievements view (problems list layout)
- `Ralph/refs/ref-investigations-documents.md` — Projects + Education views
- `Ralph/refs/ref-referrals.md` — Contact view (referral form layout)
- `Ralph/refs/ref-interactions.md` — Interactions, responsive, accessibility
- `References/CV_v4.md` — Source CV content (roles, achievements, numbers, dates) - `References/CV_v4.md` — Source CV content (roles, achievements, numbers, dates)
- `References/concept.html` — Previous ECG implementation (timing reference only for boot sequence)
Read ONLY the referenced file(s) for each task. Do NOT read goal.md directly.
## Design Document Highlights ## Design Document Highlights
+76 -95
View File
@@ -1,120 +1,101 @@
# Guardrails — Clinical Record PMR System # Guardrails
## Standard Guardrails Hard rules that MUST be followed in every iteration. Violating these will produce incorrect output.
### Frontend-design skill requirement ## Design System Guardrails
- **When**: Writing ANY component with visual styling, animations, or UI elements
- **Rule**: You MUST invoke the `/frontend-design` skill before writing code. This applies to: LoginScreen, PatientBanner, ClinicalSidebar, ClinicalAlert, all View components (Summary, Consultations, Medications, Problems, Investigations, Documents, Referrals), and any table, card, or form component.
- **Why**: The frontend-design skill provides specialized capabilities for creating polished, professional-grade visual output. This is a high-fidelity clinical interface requiring exact color matching and spacing.
### Light-mode only constraint ### When: Writing ANY visual component
- **When**: Implementing any styling for the PMR interface **Rule:** Light-mode only. Do NOT add dark mode classes, `dark:` prefixes, or theme toggles. Clinical record systems operate exclusively in light mode.
- **Rule**: This design is LIGHT-MODE ONLY. Never implement dark mode. Clinical systems operate in light mode due to high ambient lighting in consulting rooms. Use white backgrounds (`#FFFFFF`), cool light gray content areas (`#F5F7FA`), and dark text (`#111827`). **Why:** Dark mode breaks the clinical system metaphor. NHS clinical software is always light-mode due to high ambient lighting in consulting rooms.
- **Why**: Dark mode would break the clinical system metaphor entirely.
### Clinical system navigation behavior ### When: Setting border-radius on cards, inputs, or table elements
- **When**: Implementing sidebar navigation and view switching **Rule:** Use 4px border-radius (`rounded` in Tailwind, which is 4px). Do NOT use `rounded-lg` (8px), `rounded-xl` (12px), or `rounded-2xl` (16px). The only exception is the LoginScreen card which uses 12px.
- **Rule**: View switching must be INSTANT — no crossfade, no slide animation, no transition. When a sidebar item is clicked, the main content area replaces immediately. This matches EMIS Web, SystmOne, and other clinical systems exactly. **Why:** Clinical systems use minimal rounding. Larger radii look like consumer apps, not NHS software.
- **Why**: Clinical systems prioritize speed and responsiveness over visual flair. Any animation here breaks the authenticity.
### Table markup requirements ### When: Using monospace/code font
- **When**: Building the Medications, Problems, Investigations, or Documents tables **Rule:** Use Geist Mono (font-family: 'Geist Mono', monospace), NOT Fira Code, for coded entries, timestamps, clinical codes, and data values.
- **Rule**: Use proper semantic HTML `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>` elements. Headers must use `scope="col"`. Never use divs styled as tables. Tables must have `1px solid #E5E7EB` borders on all cells, 40px row height, and alternating row colors. **Why:** The spec requires Geist Mono. Fira Code was used in the ECG/boot phase but is wrong for the PMR interface.
- **Why**: Screen readers rely on proper table markup to navigate data tables. Clinical systems use real tables.
### Traffic light accessibility ### When: Adding shadows to cards or panels
- **When**: Using green/amber/red status indicators **Rule:** No shadows, or at most `0 1px 2px rgba(0,0,0,0.03)`. Do NOT use prominent shadows like `shadow-md` or `shadow-lg`.
- **Rule**: Traffic light dots (8px circles) must ALWAYS accompany text labels ("Active", "Resolved", "In Progress"). Never use color alone to communicate status. This applies to Problems status, Medications status, and Investigations status. **Why:** Clinical systems structure with borders, not shadows. Prominent shadows look like marketing sites.
- **Why**: WCAG 2.1 AA requirement — color cannot be the sole means of conveying information.
### CV content accuracy ### When: Styling borders
- **When**: Adding CV content to data files **Rule:** All card and table borders must be `1px solid #E5E7EB` (gray-200). Use `border-gray-200` in Tailwind.
- **Rule**: Use exact data from `References/CV_v4.md`. Key numbers must match: £14.6M efficiency programme, 14,000 patients, £2.6M savings, 70% reduction, 200 hours saved, £1M revenue, £220M budget. Dates must be accurate: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), etc. **Why:** This is the universal border color in NHS clinical software.
- **Why**: Inaccurate CV data is a critical error. The PMR system presents factual career information.
### NHS blue brand color ## Sidebar Label Convention
- **When**: Using the primary accent color
- **Rule**: Use exact NHS blue `#005EB8` for: active sidebar border, buttons, links, column headers, organization names. This is the actual NHS brand blue. Never use a different shade.
- **Why**: NHS blue is instantly recognizable to healthcare professionals. Wrong blue breaks the authenticity.
### TypeScript strictness ### When: Building or modifying sidebar navigation labels
- **When**: Writing any TypeScript code **Rule:** Sidebar labels MUST use CV-friendly terms: Summary, Experience, Skills, Achievements, Projects, Education, Contact. Do NOT use clinical jargon (Consultations, Medications, Problems, Investigations, Documents, Referrals) as sidebar labels. The clinical metaphor lives in the LAYOUT of each view, not the navigation labels.
- **Rule**: No `any` types. Define interfaces for all data structures in `src/types/pmr.ts`. Use proper React.FC types or function component signatures with typed props. Enable strict mode in tsconfig.json. **Why:** Non-clinical visitors should immediately understand what each section contains. The clinical system aesthetic comes from the visual presentation (consultation journal format, medications table format, etc.), not from the nav labels.
- **Why**: Type safety is critical for maintainability. The data layer has complex types (Consultation, Medication with history, Problem with codes).
### Reduced motion support ## Navigation Guardrails
- **When**: Implementing animations (login typing, alert slide, consultation expand)
- **Rule**: All animations must respect `prefers-reduced-motion: reduce`. With reduced motion: login typing completes instantly, alert appears without slide, consultation expand is instant, banner condensation is instant.
- **Why**: Accessibility requirement for users with vestibular disorders.
### No console errors ### When: Switching between sidebar views
- **When**: Writing JavaScript/TypeScript **Rule:** View switching must be INSTANT. No crossfade, no slide animation, no opacity transition between views. The main content area simply replaces its content immediately.
- **Rule**: No errors in the browser console. Handle edge cases: fuse.js search with no results, table sorting with empty data, form validation, animation cleanup on unmount. **Why:** Clinical systems use instant tab switching. Any animation makes it feel like a website, not clinical software.
- **Why**: Console errors suggest broken functionality and are a quality check failure.
### Responsive breakpoints ### When: Building navigation
- **When**: Adding responsive CSS/Tailwind classes **Rule:** URL hash routing is required. Each view must update `window.location.hash` and the app must read the hash on load to navigate to the correct view.
- **Rule**: Must work at 3 breakpoints: desktop (>1024px with full sidebar), tablet (768-1024px with icon-only sidebar), mobile (<768px with bottom nav). Tables must adapt: full columns on desktop, scrollable on tablet, card layout on mobile. **Why:** Direct linking to specific views is required for shareability.
- **Why**: Clinical records may be viewed on tablets in consulting rooms or mobile devices.
## Project-Specific Guardrails ## Component Guardrails
### ECG flatline transition ### When: Expanding/collapsing consultation entries
- **When**: Modifying ECGAnimation component **Rule:** Use height animation ONLY (200ms, ease-out). Do NOT fade opacity on the content. Content simply grows/shrinks in height.
- **Rule**: The ECG must end with a flatline (horizontal line extending rightward from the name) that visually reads as a patient monitor flatline. This transitions to the login screen background (#1E293B). Do NOT fade to white — the previous design did that, but this design requires the flatline → login sequence. **Why:** The spec explicitly states "No opacity fade — the content simply grows/shrinks."
- **Why**: The flatline signals "end of patient monitoring, opening clinical record." It's a narrative transition.
### Login typing animation ### When: Displaying traffic light status indicators
- **When**: Implementing LoginScreen component **Rule:** Traffic lights (colored dots) must ALWAYS be accompanied by text labels (Active, Resolved, In Progress, etc.). Dots are never the sole indicator of state.
- **Rule**: Username "A.CHARLWOOD" types character-by-character at 30ms per character. Password fills with 8 dots at 20ms per dot. Use Geist Mono font for the typing. Blinking cursor appears during typing. **Why:** WCAG accessibility — color cannot be the only means of communicating information.
- **Why**: The login sequence is the most immersive transition. Every NHS worker recognizes typing credentials into a clinical system.
### Consultation format fidelity ### When: Writing consultation entries
- **When**: Building ConsultationsView **Rule:** Use History / Examination / Plan section headers (uppercase, Inter 600, 12px, letter-spacing 0.05em, gray-400). Include CODED ENTRIES at the bottom of each expanded consultation in [XXX000] format.
- **Rule**: Each consultation MUST have History, Examination, and Plan sections. Use uppercase section headers with letter-spacing (Inter 600, 12px, gray-400). History = context/background, Examination = analysis/findings (bullet list), Plan = outcomes/delivery (bullet list). Include coded entries at bottom in [XXX000] format. **Why:** This is the core metaphor — SOAP notes format mapped to career content.
- **Why**: This is the clinical SOAP note format. The mapping to career content is the core concept.
### Medication table columns ### When: Rendering the clinical alert
- **When**: Building MedicationsView **Rule:** Use Framer Motion `type: "spring"` animation for the alert entrance (not ease-out). The alert uses amber colors: bg `#FEF3C7`, left border `#F59E0B`, text `#92400E`.
- **Rule**: Table must have exactly these columns: Drug Name, Dose (%), Frequency, Start (year), Status. All columns must be sortable. Default grouping: Active Medications (technical), Clinical Medications (healthcare), PRN (strategic). **Why:** The spec specifies spring animation with slight overshoot. Alerts demand attention.
- **Why**: Medications tables in clinical systems have standard columns. This mapping provides more information than typical skills sections.
### Clinical alert behavior ### When: Writing table markup
- **When**: Implementing ClinicalAlert component **Rule:** Use semantic `<table>`, `<thead>`, `<th>`, `<tbody>`, `<tr>`, `<td>` elements. Column headers must include `scope="col"`. Do NOT use div-based table layouts.
- **Rule**: Alert appears on Summary view load with spring animation (250ms). Must include warning icon, amber background (#FEF3C7), amber left border, and "Acknowledge" button. Clicking Acknowledge: icon → green checkmark (200ms) → alert collapses upward (200ms). Use `role="alert"` and `aria-live="assertive"`. **Why:** Screen readers navigate tables using native table semantics. Div tables are inaccessible.
- **Why**: The clinical alert is the signature interaction. It frames the £14.6M achievement with institutional weight.
### Coded entries format ## Data Guardrails
- **When**: Adding coded entries to consultations or problems
- **Rule**: Use fictional but consistent SNOMED-style codes: [EFF001] for efficiency, [ALG001] for algorithms, [AUT001] for automation, [SQL001] for data, [BUD001] for budget, [TRN001] for transformation, [LEA001] for leadership, etc. Codes in Geist Mono 12px, gray-500.
- **Why**: Clinical systems use coded entries (SNOMED CT, Read codes). This maintains the metaphor.
### Sidebar navigation structure ### When: Displaying CV content (dates, numbers, roles, achievements)
- **When**: Building ClinicalSidebar **Rule:** All data must come from `src/data/*.ts` files. Do NOT hardcode CV content directly in components. Do NOT change any numbers or dates — they are sourced from the verified CV.
- **Rule**: Exactly 7 items in this order: Summary, Consultations, Medications, Problems, Investigations, Documents, Referrals. Use Lucide icons: ClipboardList, FileText, Pill, AlertTriangle, FlaskConical, FolderOpen, Send. Separator line after Summary. Active state: 3px NHS blue left border. **Why:** Data accuracy is critical. The data layer has been validated against CV_v4.md.
- **Why**: This matches clinical record navigation categories. Order matters for Alt+1-7 shortcuts.
### Patient banner data ### When: Modifying data files
- **When**: Building PatientBanner **Rule:** Do NOT modify data files in `src/data/` unless the task explicitly requires it. The data is correct and complete.
- **Rule**: Full name "CHARLWOOD, Andrew (Mr)" (surname first, comma-separated). DOB "14/02/1993" (DD/MM/YYYY). NHS No "221 181 0" (GPhC number formatted like NHS number with tooltip). Address "Norwich, NR1". Status "Active" with green dot. Badge "Open to opportunities". **Why:** Data was verified in a prior iteration. Unnecessary changes risk introducing inaccuracies.
- **Why**: This is the most recognizable PMR element. Format must match clinical systems exactly.
### Keyboard shortcuts ## Visual Review Guardrails
- **When**: Implementing navigation
- **Rule**: Alt+1 through Alt+7 must activate corresponding sidebar items. Escape closes expanded items and menus. / focuses search. Implement roving tabindex in sidebar (Up/Down arrows navigate, Enter activates).
- **Why**: Clinical systems have keyboard shortcuts for rapid navigation. This is expected behavior.
### Form validation ### When: Completing any visual component task (Tasks 1b-11)
- **When**: Building ReferralsView form **Rule:** After quality checks pass, you MUST open the dev server (`http://localhost:5173`) in the browser using Claude in Chrome tools (`tabs_context_mcp`, `navigate`, `computer` with `action: "screenshot"`), take a screenshot of the relevant view, and compare against the reference file spec. Fix any visual discrepancies before committing. If browser tools are unavailable (e.g. Chrome not connected), document this in progress.txt and proceed — do NOT block the iteration.
- **Rule**: Referrer Name and Email are required. Show validation errors if empty on submit. Generate reference number in format REF-YYYY-MM-DD-NNN from current date. Success message shows reference and "Expected response time: 24-48 hours." **Why:** Code review alone cannot catch visual issues. The previous iteration loop produced functionally correct but visually generic output because no one verified the rendered result.
- **Why**: Clinical referral forms have validation. The reference number mimics real NHS referral references.
### Mobile bottom navigation ### When: Browser tools fail or Chrome is not connected
- **When**: Implementing responsive mobile layout **Rule:** If `tabs_context_mcp` or other browser tools fail, skip the visual review step, note it in progress.txt, and continue. Do NOT retry more than twice or spend time debugging browser connectivity.
- **Rule**: On mobile (<768px), sidebar becomes bottom nav bar with 7 icon buttons (56px height, safe area padding). Patient banner becomes minimal. Tables switch to card layout. Add back arrow in each view returning to Summary. **Why:** Visual review is valuable but not blocking. The loop must keep making progress.
- **Why**: Mobile clinical apps use bottom tabs. This matches the NHS App and EMIS Mobile patterns.
### Search implementation ## Technical Guardrails
- **When**: Adding search functionality
- **Rule**: Use fuse.js with threshold 0.3. Index all content: consultation titles/bullets, medication names, problem descriptions, investigation names, document titles. Group results by section. Clicking result navigates to view and expands matching item. ### When: Writing TypeScript
- **Why**: Clinical systems have record search. Fuse.js provides fuzzy matching for medical record lookups. **Rule:** No `any` types. All props must have typed interfaces. All data must use the types from `src/types/pmr.ts`.
**Why:** Strict typing prevents runtime errors and maintains code quality.
### When: Adding animations
**Rule:** All animations must respect `prefers-reduced-motion`. With reduced motion: login typing completes instantly, alerts appear without slide, expand/collapse is instant, banner condensation is instant.
**Why:** Accessibility requirement. Users who've opted out of motion must still have a functional experience.
### When: Building visual components (Tasks 1b-11)
**Rule:** Each reference file in `Ralph/refs/` contains a "Design Guidance (from /frontend-design)" section with pre-generated design direction and code patterns. Read this section BEFORE writing code. Do NOT invoke the `/frontend-design` skill at runtime — the guidance is already embedded in the ref files. Follow the aesthetic direction and code patterns provided.
**Why:** The design guidance was pre-generated to avoid context overflow. Previous iterations stalled because the skill output consumed the entire context window, leaving no room to write files.
### When: Running quality checks
**Rule:** Run `npm run typecheck`, `npm run lint`, and `npm run build` after EVERY task. Fix all errors before committing.
**Why:** Build failures compound across iterations. Fix them immediately.
+6
View File
@@ -85,3 +85,9 @@
- Kept legacy tokens in place to avoid breaking boot/ECG components - Kept legacy tokens in place to avoid breaking boot/ECG components
- Used --pmr- namespace for all PMR tokens to distinguish from legacy design system - Used --pmr- namespace for all PMR tokens to distinguish from legacy design system
- Extended Tailwind colors rather than replacing them — allows both themes to work simultaneously - Extended Tailwind colors rather than replacing them — allows both themes to work simultaneously
### IMPORTANT — Design Guidance is Pre-Baked
Do NOT invoke the `/frontend-design` skill at runtime — it was pre-run and the output is embedded in each ref file under "Design Guidance (from /frontend-design)". Previous iterations STALLED because the skill output consumed the entire context window. The guidance is now in the ref files — just read and implement.
### ECG Reference Implementation
`ECGCombined.tsx` in the project root is a Remotion version of the ECG animation with a superior mask-based text reveal technique. Task 1b references this for the canvas implementation.
+41 -11
View File
@@ -15,7 +15,8 @@
- Same error repeated N consecutive iterations (stuck) - Same error repeated N consecutive iterations (stuck)
.PARAMETER Model .PARAMETER Model
Claude model to use. Default: "opus". Initial Claude model to use. Default: "sonnet". The agent can dynamically switch
models between iterations via <next-model>opus|sonnet</next-model> signals.
.PARAMETER BranchName .PARAMETER BranchName
Optional git branch name. If provided, creates/checks out the branch before starting. Optional git branch name. If provided, creates/checks out the branch before starting.
@@ -34,7 +35,7 @@
#> #>
param( param(
[string]$Model = "opus", [string]$Model = "sonnet",
[string]$BranchName, [string]$BranchName,
[int]$MaxNoProgress = 3, [int]$MaxNoProgress = 3,
[int]$MaxSameError = 3 [int]$MaxSameError = 3
@@ -142,13 +143,40 @@ if (Test-Path $progressFile) {
Write-Host "" Write-Host ""
Write-Host "===== Ralph Wiggum Loop (Visualization Improvements) =====" -ForegroundColor Cyan Write-Host "===== Ralph Wiggum Loop (Visualization Improvements) =====" -ForegroundColor Cyan
Write-Host "Model: $Model | Runs until COMPLETE" -ForegroundColor Cyan Write-Host "Model: $Model (dynamic switching enabled) | Visual review: ON | Runs until COMPLETE" -ForegroundColor Cyan
Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan
if ($BranchName) { Write-Host "Branch: $BranchName" -ForegroundColor Cyan } if ($BranchName) { Write-Host "Branch: $BranchName" -ForegroundColor Cyan }
if ($existingIterations -gt 0) { Write-Host "Previous iterations: $existingIterations" -ForegroundColor Cyan } if ($existingIterations -gt 0) { Write-Host "Previous iterations: $existingIterations" -ForegroundColor Cyan }
Write-Host "===========================================" -ForegroundColor Cyan Write-Host "===========================================" -ForegroundColor Cyan
Write-Host "" Write-Host ""
# --- Dev Server (for visual review via Claude in Chrome) ---
$devServerPort = 5173
$devServerPid = $null
try {
$null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop
Write-Host "Dev server detected on port $devServerPort" -ForegroundColor Green
} catch {
Write-Host "Starting dev server (port $devServerPort)..." -ForegroundColor Cyan
$devProc = Start-Process -FilePath "npm.cmd" -ArgumentList "run", "dev" -PassThru -WindowStyle Minimized
$devServerPid = $devProc.Id
for ($w = 1; $w -le 20; $w++) {
Start-Sleep -Seconds 1
try {
$null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop
Write-Host "Dev server ready on port $devServerPort" -ForegroundColor Green
break
} catch {
if ($w -eq 20) {
Write-Warning "Dev server may not be ready - visual review steps may fail"
}
}
}
}
Write-Host ""
$i = 0 $i = 0
while ($true) { while ($true) {
$i++ $i++
@@ -306,6 +334,7 @@ while ($true) {
Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red
Write-Host "No git commits for $MaxNoProgress consecutive iterations. The loop is stalled." -ForegroundColor Red Write-Host "No git commits for $MaxNoProgress consecutive iterations. The loop is stalled." -ForegroundColor Red
Write-Host "Check progress.txt and logs/ for details on what went wrong." -ForegroundColor Red Write-Host "Check progress.txt and logs/ for details on what went wrong." -ForegroundColor Red
if ($devServerPid) { taskkill /T /F /PID $devServerPid 2>$null | Out-Null }
exit 1 exit 1
} }
} else { } else {
@@ -326,6 +355,7 @@ while ($true) {
Write-Host "Same error pattern for $MaxSameError consecutive iterations:" -ForegroundColor Red Write-Host "Same error pattern for $MaxSameError consecutive iterations:" -ForegroundColor Red
Write-Host " $currentErrorSignature" -ForegroundColor Red Write-Host " $currentErrorSignature" -ForegroundColor Red
Write-Host "Check progress.txt and logs/ for details." -ForegroundColor Red Write-Host "Check progress.txt and logs/ for details." -ForegroundColor Red
if ($devServerPid) { taskkill /T /F /PID $devServerPid 2>$null | Out-Null }
exit 1 exit 1
} }
} elseif ($currentErrorSignature) { } elseif ($currentErrorSignature) {
@@ -337,15 +367,14 @@ while ($true) {
$lastErrorSignature = "" $lastErrorSignature = ""
} }
# --- Push to Remote --- # --- Dynamic Model Selection ---
$hasRemote = git remote 2>$null if ($outputString -match "<next-model>(opus|sonnet)</next-model>") {
if ($hasRemote) { $nextModel = $Matches[1]
$currentBranch = git branch --show-current if ($nextModel -ne $Model) {
git push origin $currentBranch 2>$null Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta
if ($LASTEXITCODE -eq 0) { $Model = $nextModel
Write-Host " Pushed to remote." -ForegroundColor Green
} else { } else {
Write-Host " Push failed or no remote configured - continuing." -ForegroundColor DarkYellow Write-Host " [Model] Staying on $Model" -ForegroundColor DarkGray
} }
} }
@@ -354,6 +383,7 @@ while ($true) {
Write-Host "" Write-Host ""
Write-Host "===== COMPLETE =====" -ForegroundColor Green Write-Host "===== COMPLETE =====" -ForegroundColor Green
Write-Host "Visualization improvements finished after $i iteration(s) this run ($totalIteration total)." -ForegroundColor Green Write-Host "Visualization improvements finished after $i iteration(s) this run ($totalIteration total)." -ForegroundColor Green
if ($devServerPid) { taskkill /T /F /PID $devServerPid 2>$null | Out-Null }
exit 0 exit 0
} }
+145
View File
@@ -0,0 +1,145 @@
# Reference: Patient Banner + Sidebar + Navigation
> Extracted from goal.md — Patient Banner, Left Sidebar, and Navigation sections. These are the persistent UI chrome that defines the clinical system feel.
---
## Patient Banner (Persistent Top Chrome)
The patient banner is the most recognizable element of any PMR system. It spans the full viewport width above the main content area and provides constant demographic context.
### Full Banner (80px height, visible at top of page)
```
+---------------------------------------------------------------------------+
| CHARLWOOD, Andrew (Mr) Active (green dot) Open to opps. |
| DOB: 14/02/1993 | NHS No: 2211810 | Norwich, NR1 |
| 07795553088 | andy@charlwood.xyz [Download CV] [Email] [LinkedIn] |
+---------------------------------------------------------------------------+
```
### Content Mapping
| PMR Field | Actual Content | Notes |
|-----------|---------------|-------|
| Patient name | CHARLWOOD, Andrew (Mr) | Surname first, comma-separated — exactly as in clinical systems |
| DOB | 14/02/1993 | DD/MM/YYYY format (UK clinical standard) |
| NHS Number | 221 181 0 | Andy's GPhC registration number formatted like an NHS number (with spaces). Hover tooltip: "GPhC Registration Number" |
| GP Practice | Self-Referred | Tongue-in-cheek — Andy referred himself to this record |
| Address | Norwich, NR1 | Abbreviated postcode area |
| Phone | 07795553088 | Clickable (tel: link) |
| Email | andy@charlwood.xyz | Clickable (mailto: link) |
| Status | Active (green dot) | Like the "registered" status in a PMR |
| Badge | Open to opportunities | Styled as a clinical banner tag (blue background, white text, small pill shape) |
### Action Buttons (top right of banner)
| Button | PMR Equivalent | Action |
|--------|---------------|--------|
| Download CV | Print Summary | Downloads PDF version of CV |
| Email | Send Letter | Opens mailto: link |
| LinkedIn | External Link | Opens LinkedIn profile in new tab |
Buttons are styled as small outlined rectangles with NHS blue text and 1px NHS blue border, 4px radius. On hover: filled NHS blue background with white text.
### Condensed Banner (48px, sticky after scroll)
When the user scrolls past 100px of content, the banner smoothly condenses to show only the essential information on a single line:
```
CHARLWOOD, Andrew (Mr) | NHS No: 2211810 | Active (green dot) [Download CV] [Email]
```
The condensed banner sticks to the top of the viewport (`position: sticky`) with a `z-index` above the content area but below modals/alerts.
---
## Left Sidebar — Clinical Navigation
The sidebar replicates the dark navigation panel found in EMIS Web and similar clinical systems. It provides category-based access to different "record views."
**Width:** 220px (desktop), dark blue-gray (`#1E293B`) background.
### Navigation Items
**IMPORTANT:** Sidebar labels use CV-friendly terms, NOT clinical jargon. The clinical metaphor lives in the LAYOUT of each view, not the labels.
| Icon | Label | View Layout Style | Description |
|------|-------|-------------------|-------------|
| `ClipboardList` | Summary | Patient summary screen | Demographics, active items, current skills, recent role |
| `FileText` | Experience | Consultation journal layout | Reverse-chronological journal of roles with H/E/P format |
| `Pill` | Skills | Medications table layout | Skills table with proficiency dosages and frequency |
| `AlertTriangle` | Achievements | Problems list layout | Challenges resolved and ongoing, with traffic lights |
| `FlaskConical` | Projects | Investigation results layout | Project outcomes with status badges |
| `FolderOpen` | Education | Attached documents layout | Certificates and qualifications |
| `Send` | Contact | Referral form layout | Contact/message form styled as clinical referral |
### Styling
- Each item: 44px height, 16px left padding, icon (18px, `lucide-react`) + label in Inter 500, 14px
- Default state: white text at 70% opacity, transparent background
- Hover state: white text at 100% opacity, background `rgba(255,255,255,0.08)`
- Active state: white text at 100%, NHS blue left border (3px), background `rgba(255,255,255,0.12)`, label in Inter 600
- A thin horizontal separator line (`1px solid rgba(255,255,255,0.1)`) appears between "Summary" and "Consultations" (separating the overview from the detail views)
### Sidebar Footer
At the bottom of the sidebar, in small text (Inter 400, 11px, `#64748B`):
```
Session: A.CHARLWOOD
Logged in: [current time]
```
This updates with the actual current time on mount, reinforcing the "logged in" metaphor.
### Sidebar Header
At the top, above the navigation items, a small logo or system name:
```
CareerRecord PMR
v1.0.0
```
In Inter 500, 13px, white at 50% opacity. Styled like the "EMIS Web" branding that appears in the top-left of the real system.
---
## Navigation
### Primary Navigation: Left Sidebar
The sidebar is always visible on desktop — this is how clinical systems work. There is no floating nav, no hamburger menu on desktop, and no scroll-based navigation. The sidebar provides persistent, direct access to any record section.
### Keyboard Shortcuts
| Sidebar Item | View Layout | Shortcut |
|-------------|-------------|----------|
| Summary | Patient summary | `Alt+1` |
| Experience | Consultation journal | `Alt+2` |
| Skills | Medications table | `Alt+3` |
| Achievements | Problems list | `Alt+4` |
| Projects | Investigation results | `Alt+5` |
| Education | Attached documents | `Alt+6` |
| Contact | Referral form | `Alt+7` |
### URL Hash Routing
Each sidebar item updates the URL hash (`#summary`, `#experience`, `#skills`, `#achievements`, `#projects`, `#education`, `#contact`) for direct linking. On page load, the app reads the hash and navigates to the corresponding view.
### Breadcrumb
A breadcrumb appears at the top of the main content area:
```
Patient Record > Consultations > Interim Head, Population Health
```
The breadcrumb updates as the user navigates deeper (e.g., expanding a consultation). Clicking "Patient Record" returns to Summary. Clicking "Consultations" collapses any expanded entries and shows the full journal list. The breadcrumb is styled in Inter 400, 13px, gray-400, with chevron separators.
### Secondary Navigation: Within-View Interactions
- **Summary:** Clicking "View Full List" or "View Full Record" links navigates to the corresponding sidebar section.
- **Consultations:** Expand/collapse individual entries. "Linked consultations" in Problems view can deep-link to specific consultation entries.
- **Medications:** Category tabs (Active, Clinical, PRN) within the view. Click to expand prescribing history.
- **Problems:** Click to expand. "Linked consultations" navigate to Consultations view.
- **Investigations:** Click to expand results.
- **Documents:** Click to expand preview.
- **Referrals:** No sub-navigation.
+359
View File
@@ -0,0 +1,359 @@
# Reference: Boot Sequence + ECG Animation
> Covers the full pre-login flow: terminal boot → cursor transition → ECG heartbeat → name reveal → flatline. The flatline→login transition is covered in `ref-transition-login.md`.
---
## Current Architecture
Two components manage the pre-login flow:
- `src/components/BootSequence.tsx` → terminal text animation, ends with blinking cursor
- `src/components/ECGAnimation.tsx` → canvas-based heartbeat + name tracing + flatline + bg transition
- `App.tsx` phases: `boot → ecg → login → pmr`
## What Needs to Change
### 1. Boot Sequence — Clean Up for Easy Config
**Problem:** Boot text lines are hardcoded as HTML strings with inline Tailwind classes. Adding/removing/reordering lines requires editing raw HTML. The `dangerouslySetInnerHTML` approach is fragile.
**Fix:** Refactor to a clean config-driven structure:
```typescript
// Example config structure — easy to customize
const BOOT_CONFIG = {
header: { text: 'CLINICAL TERMINAL v3.2.1', style: 'bright' },
lines: [
{ type: 'status', text: 'Initialising pharmacist profile...' },
{ type: 'separator' },
{ type: 'field', label: 'SYSTEM', value: 'NHS Norfolk & Waveney ICB' },
{ type: 'field', label: 'USER', value: 'Andy Charlwood' },
{ type: 'field', label: 'ROLE', value: 'Deputy Head of Population Health & Data Analysis' },
{ type: 'field', label: 'LOCATION', value: 'Norwich, UK' },
{ type: 'separator' },
{ type: 'status', text: 'Loading modules...' },
{ type: 'module', name: 'pharmacist_core.sys' },
{ type: 'module', name: 'population_health.mod' },
{ type: 'module', name: 'data_analytics.eng' },
{ type: 'separator' },
{ type: 'ready', text: 'READY — Rendering CV..' },
],
timing: { lineDelay: 220, holdAfterComplete: 400, fadeOutDuration: 800 },
}
```
- Each line type maps to a React component (not raw HTML)
- Colors remain: bright green `#00ff41`, dim green `#3a6b45`, cyan labels `#00e5ff`
- Staggered reveal timing stays the same (220ms per line)
- Font: Fira Code (this is the terminal phase, NOT the PMR — Fira Code is correct here)
### 2. Cursor → Dot Transition
**Problem:** The boot sequence ends with a blinking green block cursor (`.animate-blink`). The ECG animation starts with a glowing dot that appears at the far left of the screen. There's a visual disconnect — the cursor and dot don't connect.
**Fix:** The blinking cursor at the end of boot should smoothly transition INTO the ECG's glowing trace dot:
- At end of boot, capture the cursor's screen position (x, y)
- Pass this position to ECGAnimation via props
- ECGAnimation starts with its glowing dot AT the cursor position
- The cursor stops blinking and morphs: block cursor → circular glow (scale down width, increase glow, ~300ms)
- The dot then begins moving rightward, drawing the flatline/heartbeat trace behind it
- This means the ECG trace starts at the cursor position, NOT the far left edge
### 3. ECG Start Position
**Problem:** Currently the ECG trace starts at x=0 (far left of viewport). The cursor ends somewhere in the middle-left of the screen. This means the dot "teleports" from cursor position to the left edge.
**Fix:** The ECG animation should:
- Accept a `startPosition: { x: number, y: number }` prop from the boot sequence
- Begin the trace from that position
- The first section of trace is a flat line from the cursor position rightward
- Heartbeats begin after the first flat gap (same timing as now, just offset)
- The viewport scroll logic already handles the "head" position — just shift the world-space origin
### 4. Text Reveal — Mask Technique
**Problem:** The current ECGAnimation.tsx reveals letters by stroking them with progressive alpha (`letterProgress > 0.3` → fade in). This looks like letters fading in, not like the ECG line IS the letter shape.
**Reference:** `ECGCombined.tsx` (Remotion version at project root) uses a superior technique:
- The actual text characters are pre-rendered on the canvas (stroke-only, no fill)
- A wipe mask follows the ECG trace head, revealing the text underneath
- The ECG trace line IS the path that forms each letter shape (via the `getYAtX` function which returns letter Y values when in text region)
- Connector lines between letters sit at the baseline
- The neon glow filter applies to both the trace and revealed text
**What to adopt from ECGCombined.tsx:**
- The mask-based text reveal approach (the trace unveils pre-rendered text)
- The connector lines between letters at baseline
- The neon glow SVG filters (or canvas equivalents)
- The text mask brush that follows the trace head
**What to KEEP from current ECGAnimation.tsx:**
- The character spacing (current `LETTER_W`, `LETTER_G`, `SPACE_W` values — preferred over ECGCombined.tsx spacing)
- The heartbeat waveform shape (`generateHeartbeatPoints`)
- The beat timing and amplitude escalation (0.3 → 0.55 → 0.85 → 1.0)
- The canvas rendering approach (not SVG — canvas is correct for this performance-sensitive animation)
- The viewport scrolling/camera logic
- The flatline draw phase
- The scanline and vignette effects
- The background color transition to `#1E293B`
### 5. Text Rendering
- The name is still "ANDREW CHARLWOOD"
- Letters are stroke-only (no fill) in neon green `#00ff41`
- Each letter shape is defined by the `ECG_LETTERS` point arrays (keep these)
- The trace line passes through each letter's shape points, making the ECG waveform form recognizable letter shapes
- Between letters, the trace returns to baseline with short connector segments
- Neon glow effect on both trace and revealed text
## Transition to Login
After the text is fully revealed and the flatline extends to the right edge, the flow continues as described in `ref-transition-login.md`:
1. Name holds with glow (300ms)
2. Glow fades, flatline extends right
3. Canvas fades to black (200ms)
4. Background transitions to dark blue-gray `#1E293B` (200ms)
5. Login card materializes
## prefers-reduced-motion
With reduced motion enabled:
- Boot text appears instantly (no stagger)
- Cursor appears immediately
- ECG animation is skipped entirely — transition straight from boot to login
- Or: show the final frame (name fully revealed, flatline) as a static image for 1 second, then proceed to login
## Testing Checklist
- [ ] Boot text renders correctly with all lines
- [ ] Blinking cursor visible at end of boot
- [ ] Cursor smoothly transitions to ECG dot (no teleport)
- [ ] ECG trace starts from cursor position
- [ ] Heartbeats render with increasing amplitude
- [ ] Name letters revealed via mask technique (not alpha fade)
- [ ] Connector lines between letters
- [ ] Neon glow on trace and text
- [ ] Flatline extends to right edge after name
- [ ] Background transitions to `#1E293B`
- [ ] Scanlines and vignette present
- [ ] Reduced motion: instant/static
- [ ] Mobile: scales correctly
---
## Design Guidance (from /frontend-design)
> Pre-baked design direction. Do NOT invoke `/frontend-design` at runtime — this section contains the output.
### Aesthetic Direction: Authentic Clinical Terminal → Medical Monitor Realism
This isn't "medical-themed" design — this IS medical equipment interfaces. Two distinct phases:
1. **Boot Terminal**: Authentic 1990s clinical system boot sequence (think legacy pharmacy dispensing systems, hospital terminal logins). CRT monitor aesthetic with phosphor green, scanlines, slight text glow.
2. **ECG Monitor**: Hospital bedside cardiac monitor realism. The heartbeat isn't decorative — it's a functioning waveform that becomes letterforms through technical precision.
**Visual Signature**: The cursor-to-dot morphing transition. Most animations have discrete phases; this creates continuous material transformation — the blinking terminal cursor literally becomes the ECG trace point. It's the moment where "loading system" becomes "reading vital signs."
### Boot Sequence — Type-Safe Config
```typescript
type BootLineType = 'header' | 'status' | 'separator' | 'field' | 'module' | 'ready'
interface BootLine {
type: BootLineType
text: string
label?: string
value?: string
style?: 'bright' | 'dim' | 'cyan'
}
interface BootConfig {
header: string
lines: BootLine[]
timing: {
lineDelay: number
cursorBlinkInterval: number
holdAfterComplete: number
fadeOutDuration: number
}
colors: {
bright: string
dim: string
cyan: string
}
}
```
Component architecture:
- `<BootLine>` — renders individual line types with appropriate styling
- `<BootCursor>` — separate component for cursor with ref exposure for position capture
- Config-driven rendering replaces hardcoded HTML
Example config:
```typescript
const BOOT_CONFIG: BootConfig = {
header: 'CLINICAL TERMINAL v3.2.1',
lines: [
{ type: 'status', text: 'Initialising pharmacist profile...', style: 'dim' },
{ type: 'separator', text: '---', style: 'dim' },
{ type: 'field', label: 'SYSTEM', value: 'NHS Norfolk & Waveney ICB', style: 'cyan' },
{ type: 'field', label: 'USER', value: 'Andy Charlwood', style: 'bright' },
// ... etc
],
timing: { lineDelay: 220, cursorBlinkInterval: 530, holdAfterComplete: 400, fadeOutDuration: 800 },
colors: { bright: '#00ff41', dim: '#3a6b45', cyan: '#00e5ff' }
}
```
Example BootLine component:
```typescript
function BootLine({ line }: { line: BootLine }) {
const colors = BOOT_CONFIG.colors
const color = line.style ? colors[line.style] : colors.dim
if (line.type === 'field') {
return (
<div className="font-mono text-sm leading-relaxed">
<span style={{ color: colors.cyan }}>{line.label?.padEnd(9)}</span>
<span style={{ color }}>{line.value}</span>
</div>
)
}
if (line.type === 'module') {
return (
<div className="font-mono text-sm leading-relaxed">
<span className="font-bold" style={{ color: colors.bright }}>[OK]</span>
{' '}
<span style={{ color: colors.dim }}>{line.text}</span>
</div>
)
}
// ... other types
}
```
### Cursor → Dot Transition — Implementation
```typescript
const [cursorPosition, setCursorPosition] = useState<{x: number, y: number} | null>(null)
const cursorRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (cursorRef.current) {
const rect = cursorRef.current.getBoundingClientRect()
setCursorPosition({
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
})
}
}, [/* trigger when boot completes */])
// Pass to ECG:
<ECGAnimation startPosition={cursorPosition} onComplete={...} />
```
Morph animation: width 8px→0px, height 16px→6px, border-radius 0→50% (300ms ease-out). Simultaneously fade in ECG dot at same position. After morph complete, begin trace movement.
### Canvas Mask Reveal — Implementation
```javascript
// Pre-render text to offscreen canvas
const textCanvas = document.createElement('canvas')
const textCtx = textCanvas.getContext('2d')
textCtx.strokeStyle = lineColor
textCtx.lineWidth = 1.5
textCtx.font = `bold ${fontSize}px Arial`
textLayout.forEach(item => {
textCtx.strokeText(item.char, item.centerX, baselineY)
})
// During animation loop:
ctx.save()
// Create clipping path following trace head
ctx.beginPath()
ctx.rect(0, 0, headSX + 20, vh) // reveal up to head position + lead
ctx.clip()
// Draw pre-rendered text through clip
ctx.drawImage(textCanvas, -viewOff, 0)
ctx.restore()
// Feathered leading edge:
const gradient = ctx.createLinearGradient(headSX - 30, 0, headSX, 0)
gradient.addColorStop(0, 'rgba(0,255,65,0)')
gradient.addColorStop(1, 'rgba(0,255,65,1)')
```
### Connector Lines Between Letters
```javascript
const connectors: {startX: number, endX: number}[] = []
for (let i = 0; i < textLayout.length - 1; i++) {
const curr = textLayout[i]
const next = textLayout[i + 1]
const endInset = CONNECTOR_INSETS[curr.char] || 0
const startInset = CONNECTOR_INSETS[next.char] || 0
connectors.push({
startX: curr.endX - endInset,
endX: next.startX + startInset
})
}
// During draw:
connectors.forEach(conn => {
if (headWX > conn.startX) {
const connectorEndX = Math.min(conn.endX, headWX)
ctx.beginPath()
ctx.moveTo(conn.startX - viewOff, baselineY)
ctx.lineTo(connectorEndX - viewOff, baselineY)
ctx.stroke()
}
})
```
### Visual Enhancement Details
**CRT Scanlines** (boot phase):
```css
.boot-scanlines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15) 0px,
transparent 1px,
transparent 2px,
rgba(0, 0, 0, 0.15) 3px
);
pointer-events: none;
animation: scanline-drift 8s linear infinite;
}
```
**Phosphor Glow** (terminal text):
```css
.terminal-text {
text-shadow:
0 0 4px currentColor,
0 0 8px currentColor,
0 0 12px rgba(0, 255, 65, 0.3);
}
```
**ECG Neon Glow** (canvas — multi-layer):
- Primary trace: 2px solid line
- Glow layer 1: 6px @ 25% opacity + shadowBlur 14px
- Glow layer 2: Inner 3px core for sharpness
- Text: Same multi-layer glow approach
**Background Transition** (smooth RGB interpolation):
```javascript
const bgProgress = (elapsed - flatlineStartTime) / BG_TRANSITION
ctx.fillStyle = `rgb(
${Math.round(0 + (30 * bgProgress))},
${Math.round(0 + (41 * bgProgress))},
${Math.round(0 + (59 * bgProgress))}
)` // black → #1E293B
ctx.fillRect(0, 0, vw, vh)
```
+109
View File
@@ -0,0 +1,109 @@
# Reference: Consultations View (= Experience)
> Extracted from goal.md — Consultations View section. Each role is a "consultation entry" in a reverse-chronological journal.
---
## Overview
This is the core content view and the most detailed section. Each role is a "consultation entry" in a reverse-chronological journal.
## Journal List Layout
Entries are stacked vertically, most recent at top. Each entry has a collapsed state and an expanded state.
### Collapsed Entry
```
+------------------------------------------------------------------+
| (green dot) 14 May 2025 | NHS Norfolk & Waveney ICB |
| Interim Head, Population Health & Data Analysis |
| Key: 14.6M efficiency programme identified and delivered |
| [v Expand] |
+------------------------------------------------------------------+
```
- Date in Geist Mono, 13px, gray-500 (left-aligned)
- Organization in Inter 400, 13px, NHS blue
- Role title in Inter 600, 15px, gray-900
- Key coded entry: a single-line summary of the most notable achievement, prefixed with "Key:" in Inter 500, gray-500
- Expand chevron button (right-aligned)
- Status dot: green for current roles, gray for historical
### Expanded Entry (click to expand)
```
+------------------------------------------------------------------+
| (green dot) 14 May 2025 | NHS Norfolk & Waveney ICB [^ Close] |
| Interim Head, Population Health & Data Analysis |
| Duration: May 2025 - Nov 2025 |
| |
| HISTORY |
| Returned to substantive Deputy Head role following |
| commencement of ICB-wide organisational consultation. |
| Led strategic delivery of population health initiatives |
| and data-driven medicines optimisation across Norfolk & |
| Waveney ICS, reporting to Associate Director of Pharmacy. |
| |
| EXAMINATION |
| - Identified 14.6M efficiency programme through |
| comprehensive data analysis |
| - Built Python-based switching algorithm: 14,000 patients |
| identified, 2.6M annual savings |
| - Automated incentive scheme analysis: 50% reduction |
| in targeted prescribing within 2 months |
| |
| PLAN |
| - Achieved over-target performance by October 2025 |
| - 2M on target for delivery this financial year |
| - Presented to CMO bimonthly with evidence-based |
| recommendations |
| - Led transformation to patient-level SQL analytics |
| |
| CODED ENTRIES |
| [EFF001] Efficiency programme: 14.6M identified |
| [ALG001] Algorithm: 14,000 patients, 2.6M savings |
| [AUT001] Automation: 50% prescribing reduction in 2mo |
| [SQL001] Data transformation: practice->patient level |
+------------------------------------------------------------------+
```
## The History / Examination / Plan Structure
This is a direct mapping from the clinical consultation format (SOAP notes) to career content:
| Clinical Term | CV Mapping | What Goes Here |
|--------------|------------|----------------|
| **History** | Context / Background | Why this role existed, what situation Andy walked into, reporting lines |
| **Examination** | Analysis / Findings | What Andy discovered, built, or analyzed — the technical and analytical work |
| **Plan** | Outcomes / Delivery | What was achieved, what impact was measured, what's ongoing |
Section headers ("HISTORY", "EXAMINATION", "PLAN") are styled in Inter 600, 12px, uppercase, letter-spacing 0.05em, gray-400 — exactly like the section dividers in a clinical consultation record.
## Coded Entries
At the bottom of each expanded consultation, "coded entries" appear — short-form tagged achievements with bracket codes. These mimic SNOMED CT / Read codes used in clinical systems. The codes are fictional but consistent (EFF = efficiency, ALG = algorithm, AUT = automation, SQL = data, etc.). Styled in Geist Mono, 12px, gray-500, with the code in brackets and the description after.
## Color Coding by Employer
Each consultation entry has a subtle left border (3px) indicating the employer:
- NHS Norfolk & Waveney ICB: NHS blue (`#005EB8`)
- Tesco PLC: Teal (`#00897B`)
This visual grouping helps the user quickly scan which organization each entry belongs to, without reading the text.
## Full Consultation Journal (all roles)
| Date | Organization | Role | Key Coded Entry |
|------|-------------|------|-----------------|
| May 2025 | NHS N&W ICB | Interim Head, Pop. Health & Data Analysis | [EFF001] 14.6M efficiency programme |
| Jul 2024 | NHS N&W ICB | Deputy Head, Pop. Health & Data Analysis | [BUD001] 220M budget management |
| May 2022 | NHS N&W ICB | High-Cost Drugs & Interface Pharmacist | [AUT002] Blueteq automation: 70% reduction |
| Nov 2017 | Tesco PLC | Pharmacy Manager | [INN001] Asthma screening: ~1M national revenue |
| Aug 2016 | Tesco PLC | Duty Pharmacy Manager | [REG001] GPhC registration commenced |
## Animation Behavior
- **Expand/collapse:** Height animation, 200ms, ease-out. No opacity fade — the content simply grows/shrinks.
- Only one consultation can be expanded at a time. Expanding a new entry collapses the previous one.
- The expand chevron rotates 180 degrees (pointing up when expanded).
+87
View File
@@ -0,0 +1,87 @@
# Reference: Visual Design System
> Extracted from goal.md — Visual System section. This is the SINGLE SOURCE OF TRUTH for colors, typography, spacing, borders, and motion throughout the Clinical Record PMR.
---
## Color Palette
This design is **light-mode only**. Clinical record systems operate in light mode — high ambient lighting in consulting rooms demands white backgrounds and dark text. A dark mode would break the metaphor.
**Backgrounds:**
- Main content area: `#F5F7FA` (cool light gray — the content background of EMIS/SystmOne)
- Card/panel surfaces: `#FFFFFF` (white)
- Sidebar: `#1E293B` (dark blue-gray — EMIS-style dark navigation panel)
- Patient banner: `#334155` (lighter blue-gray with white text)
- Login screen background: `#1E293B` (same as sidebar — institutional dark blue-gray)
**Text:**
- Primary text: `#111827` (gray-900 — near-black for maximum readability)
- Secondary text: `#6B7280` (gray-500)
- On dark surfaces: `#FFFFFF` (white) and `#94A3B8` (slate-400 for secondary)
**Accent and status colors:**
- NHS blue: `#005EB8` — primary interactive color. Used for buttons, active nav states, links, column headers. This is the actual NHS brand blue and will be instantly recognized.
- Green: `#22C55E` — active/resolved/current states. "Active" status dots, resolved problems, current role indicators.
- Amber: `#F59E0B` — alerts, in-progress items, notable achievements. The clinical alert banner uses this as its background.
- Red: `#EF4444` — urgent/critical markers. Used sparingly — only for genuinely important items (e.g., a "priority" flag on the referral form).
- Gray: `#6B7280` — inactive/historical items. Past roles that are no longer current, historical "medications."
**Traffic light system (used throughout):**
- Green circle: Active / Resolved / Current
- Amber circle: In progress / Alert / Notable
- Red circle: Urgent / Critical (rare)
- Gray circle: Inactive / Historical
## Typography
Clinical systems use system fonts — Inter or Segoe UI for general text, monospace for coded entries and data values. No decorative fonts, no variable tracking. Functional typography optimized for scanning dense tables.
- **Patient banner name:** Inter 600, 20px (not huge — clinical systems don't emphasize the patient name with large type)
- **Patient banner details:** Inter 400, 14px
- **Sidebar navigation labels:** Inter 500, 14px, white
- **Section headings (within main area):** Inter 600, 18px
- **Consultation entry titles:** Inter 600, 16px
- **Body text / descriptions:** Inter 400, 14px, line-height 1.6
- **Table headers:** Inter 600, 13px, uppercase, letter-spacing 0.03em, gray-500
- **Table data cells:** Inter 400, 14px
- **Coded entries / data values:** Geist Mono 400, 13px
- **Clinical codes (SNOMED-style):** Geist Mono 400, 12px, gray-400
- **Timestamps:** Geist Mono 400, 12px
- **Alert banner text:** Inter 500, 14px
## Spacing and Layout
- **Sidebar width:** 220px (fixed, desktop). Collapses to 56px (icon-only) on tablet.
- **Patient banner height:** 80px (full), 48px (condensed/sticky)
- **Main content max-width:** No max-width — clinical systems fill available space. Content flows within the area between sidebar and viewport edge.
- **Main content padding:** 24px
- **Card padding:** 16px (clinical systems are more compact than marketing sites)
- **Border radius:** 4px for cards and inputs (clinical systems use minimal rounding — 4px, not 12px or 16px)
- **Table row height:** 40px
- **Section spacing:** 24px between content blocks
- **Base unit:** 4px — tighter spacing than typical, reflecting clinical system density
## Borders and Surfaces
Borders are the dominant visual structuring element. Clinical systems do not rely on shadows or negative space — they use explicit borders to delineate every element.
- **All cards:** `1px solid #E5E7EB` (gray-200) border, `4px` border-radius, no shadow (or at most `0 1px 2px rgba(0,0,0,0.03)`)
- **Table cells:** `1px solid #E5E7EB` borders (all sides)
- **Sidebar border:** `1px solid #334155` (subtle right border in a slightly lighter shade)
- **Patient banner border:** `1px solid #475569` bottom border
- **Input fields:** `1px solid #D1D5DB` border, `4px` radius, `#FFFFFF` background, `8px 12px` padding
- **Active/selected rows:** `#EFF6FF` background (very subtle blue tint) — this is how EMIS highlights the selected row
## Motion
Clinical systems are fast and functional. Animations are minimal and purposeful — no spring physics, no bouncy transitions. Everything is immediate or uses simple ease-out.
- **Navigation switches:** Instant content swap. No crossfade, no slide. When you click a sidebar item, the main content area replaces immediately — just like clicking a tab in EMIS.
- **Consultation expand/collapse:** Height animation, 200ms, `ease-out`. No opacity fade — the content simply grows/shrinks.
- **Alert banner entrance:** Slide down from top, 250ms, with a subtle spring overshoot (this is the one exception — alerts are meant to demand attention).
- **Alert acknowledge:** The alert shrinks in height to zero (200ms) with a small green checkmark that flashes briefly.
- **Hover states:** Background-color transitions, 100ms. No transforms, no lifts. Just color.
- **Login typing:** Character-by-character reveal using `setInterval` (30ms per character for username, 20ms per dot for password).
- **Patient banner scroll condensation:** Smooth height transition (200ms) from full (80px) to condensed (48px) as user scrolls past the first 100px of content.
- **`prefers-reduced-motion`:** Typing animation completes instantly (full text appears), alert slides are replaced with fade-in, expand/collapse is instant.
+162
View File
@@ -0,0 +1,162 @@
# Reference: Interactions, Responsive Design, and Accessibility
> Extracted from goal.md — Interactions, Responsive Strategy, and Accessibility sections.
---
## Interactions and Micro-interactions
### Sidebar Navigation
- Clicking a sidebar item instantly swaps the main content area. No crossfade, no transition — just an immediate swap. This matches clinical system behavior exactly: navigation is instant.
- The active sidebar item updates its left border (3px, NHS blue) and background tint simultaneously, with no animation (instant state change).
### Consultation Expand / Collapse
- Clicking a consultation entry toggles between collapsed and expanded states.
- The expand animation: height grows from 0 to content height over 200ms, ease-out. Content opacity transitions from 0 to 1 over the same duration.
- Only one consultation can be expanded at a time. Expanding a new entry collapses the previous one.
- The expand chevron rotates 180 degrees (pointing up when expanded).
### Medication Row Hover
- Hovering a medication table row changes its background to `#EFF6FF` (subtle blue tint).
- No transform, no elevation change. Just color.
### Table Column Sorting
- Clicking a table column header sorts by that column. An arrow indicator (up/down) appears in the header.
- Clicking the same header again reverses sort direction.
- Sorting is instant (no animation on row reordering).
### Patient Banner Scroll Condensation
- As the user scrolls past 100px of content, the patient banner smoothly transitions from full (80px) to condensed (48px) over 200ms.
- The condensed banner shows only: name, NHS number, status dot, and action buttons.
- Scrolling back to top restores the full banner.
- Uses `position: sticky` with an `IntersectionObserver` to trigger the condensation.
### Alert Acknowledge
- Clicking "Acknowledge" on a clinical alert:
1. The warning icon cross-fades to a green checkmark (200ms)
2. After a 200ms hold, the alert's height animates to 0 (200ms, ease-out)
3. Content below shifts upward to fill the space (same 200ms timing)
### Search
- A search input in the sidebar header ("Search record...") provides fuzzy matching across all PMR sections.
- Typing shows a dropdown of results grouped by section (Consultations, Medications, Problems, etc.).
- Each result shows the section icon, the matching text, and a relevance indicator.
- Pressing Enter or clicking a result navigates to that section with the matching item highlighted/expanded.
- Implementation: fuse.js for fuzzy search across a pre-built index of all content.
### Context Menus
- Right-clicking (desktop) or long-pressing (mobile) on certain elements reveals a context menu:
- On a consultation entry: "Expand", "Copy to clipboard", "View coded entries"
- On a medication row: "View prescribing history", "Copy to clipboard"
- On a problem entry: "View linked consultations", "Copy to clipboard"
- Context menus styled: white background, `1px solid #E5E7EB` border, 4px radius, `box-shadow: 0 4px 12px rgba(0,0,0,0.1)`. Items in Inter 400, 14px, 36px row height.
### Login Screen Typing
- The username types character-by-character (30ms per character).
- The password dots appear faster (20ms per dot).
- A blinking cursor appears in the active field (530ms blink interval).
- The "Log In" button shows a brief active/pressed state before the interface materializes.
---
## Responsive Strategy
### Desktop (>1024px)
The full PMR experience. This is the design's primary target — clinical systems are desktop applications.
- Sidebar: 220px, always visible, dark blue-gray
- Patient banner: full width, 80px height, condensing to 48px on scroll
- Main content: fills remaining width (no max-width constraint)
- Tables: full column display, alternating row colors, sort controls
- Consultations: full History/Examination/Plan expanded view
- Search: integrated in sidebar header
### Tablet (768-1024px)
Sidebar collapses to icon-only mode (56px width). Hovering or tapping an icon shows the label as a tooltip.
- Patient banner: condensed to single-line format always (no full/condensed toggle)
- Main content: nearly full width
- Tables: may horizontally scroll if columns exceed available width
- Context menus: triggered by long-press instead of right-click
### Mobile (<768px)
The sidebar becomes a **bottom navigation bar** with 7 icon buttons.
**Bottom nav layout:**
```
[Summary] [Consult] [Meds] [Problems] [Invest] [Docs] [Refer]
```
Each icon from Lucide, 20px, with the active item highlighted in NHS blue with a label below. Height: 56px with safe area padding.
**Patient banner on mobile:** Minimal top bar: `CHARLWOOD, A (Mr) | 2211810 | (dot)` — action buttons collapse into "..." overflow menu.
**Content adaptations:**
- Tables switch to card layout: each row becomes a small card with fields stacked vertically
- Consultation entries: tap-to-expand pattern with larger tap targets (48px minimum height)
- Medications: table becomes stacked card list
- Referral form: full-width inputs, generous touch targets
- Search: moves to top of each view as a search bar
**Back navigation:** Each view has a back arrow returning to Summary.
### Breakpoint Summary
| Element | Desktop (>1024) | Tablet (768-1024) | Mobile (<768) |
|---------|-----------------|-------------------|---------------|
| Sidebar | 220px, full labels | 56px, icons only | Bottom nav bar |
| Patient banner | 80px full / 48px sticky | 48px always | Minimal top bar |
| Tables | Full columns, horizontal | Scroll if needed | Card layout (stacked) |
| Search | Sidebar header | Sidebar header | Top of each view |
| Context menus | Right-click | Long-press | Long-press |
---
## Accessibility
### Semantic HTML
- Sidebar: `<nav role="navigation" aria-label="Clinical record navigation">` with `<ul>` and `<li>` items. Active item uses `aria-current="page"`.
- Patient banner: `<header role="banner">` containing patient demographics.
- Main content area: `<main>` element with `aria-label` matching the current view name.
- Tables: Proper `<table>`, `<thead>`, `<th>`, `<tbody>`, `<tr>`, `<td>` markup. Column headers use `scope="col"`.
- Consultation entries: `<article>` elements with `<button>` for expand/collapse, `aria-expanded` attribute.
### Keyboard Navigation
- `Tab` moves between: sidebar items, patient banner buttons, main content interactive elements
- `ArrowUp` / `ArrowDown` within the sidebar moves between navigation items (roving tabindex)
- `Enter` / `Space` on sidebar items activates that view
- `Enter` / `Space` on consultation entries toggles expand/collapse
- `Alt+1` through `Alt+7` directly activates sidebar items
- `Escape` closes expanded items, context menus, and search dropdown
- Search input focusable with `/` key
### Screen Reader Experience
1. After login, announces: "Patient Record for Charlwood, Andrew. Summary view."
2. Clinical alert announced via `role="alert"`: full alert text
3. Tables announced with column headers
4. Expandable items announce expanded/collapsed state
5. Breadcrumb uses `<nav aria-label="Breadcrumb">`
### Alert Accessibility
- Uses `role="alert"` and `aria-live="assertive"`
- Acknowledge button: `aria-label="Acknowledge clinical alert"`
- Removal is smooth (element removes from accessibility tree)
### Focus Management
- After login: focus moves to first sidebar item (Summary)
- After navigating to new view: focus moves to first heading in main content
- After expanding consultation: focus moves to HISTORY heading
- After closing context menu: focus returns to trigger element
- After acknowledging alert: focus moves to main content first interactive element
### Color and Contrast
- All text meets WCAG 2.1 AA contrast requirements
- Traffic lights never sole communicator — always with text labels
- NHS blue on white: ~7.3:1 contrast ratio
- Amber alert text on amber bg: ~5.8:1 contrast ratio
### Motion Preferences
When `prefers-reduced-motion: reduce`:
- Login typing completes instantly
- Alert appears without slide
- Expand/collapse is instant
- Banner condensation is instant
- Hover background-color changes remain
+108
View File
@@ -0,0 +1,108 @@
# Reference: Investigations View (= Projects) + Documents View (= Education)
> Extracted from goal.md — Investigations and Documents sections. Two simpler views that share the expandable-row pattern.
---
## Investigations View (= Projects)
Projects presented as diagnostic investigations — tests that were ordered, performed, and returned results.
### Investigation List
```
+--[ Investigation Results ]----------------------------------------------+
| Test Name | Requested | Status | Result |
|------------------------------+-----------+----------+-------------------|
| PharMetrics Interactive | 2024 | Complete | Live (green) |
| Platform | | | |
| Patient Switching Algorithm | 2025 | Complete | 14,000 pts found |
| Blueteq Generator | 2023 | Complete | 70% reduction |
| CD Monitoring System | 2024 | Complete | Population-scale |
| Sankey Chart Analysis Tool | 2023 | Complete | Pathway audit |
| Patient Pathway Analysis | 2024 | Ongoing | In development |
+-------------------------------------------------------------------------+
```
### Status Badges
Styled like laboratory result status indicators:
- **Complete** (green dot): Investigation finished, results available
- **Ongoing** (amber dot): Investigation still in progress
- **Live** (pulsing green dot): Results are actively being used (for PharMetrics, which is a live URL)
### Expanded Investigation View
Clicking an investigation row reveals a detailed "results panel" below the row:
```
PharMetrics Interactive Platform
|-- Date Requested: 2024
|-- Date Reported: 2024
|-- Status: Complete - Live at medicines.charlwood.xyz
|-- Requesting Clinician: A. Charlwood
|-- Methodology:
| Real-time medicines expenditure dashboard providing
| actionable analytics for NHS decision-makers. Built with
| Power BI and SQL, tracking expenditure across the 220M
| prescribing budget.
|-- Results:
| - Real-time tracking of medicines expenditure
| - Actionable analytics for budget holders
| - Self-serve model for wider team
|-- Tech Stack: Power BI, SQL, DAX
|-- [View Results ->] (external link to medicines.charlwood.xyz)
```
The expanded view uses a tree-like indented structure (with box-drawing characters in monospace) to present the investigation report. This mirrors how lab results and imaging reports appear in clinical systems — structured, indented, with labelled fields.
### "View Results" Link
For PharMetrics (the only project with a live URL), a "View Results" button appears styled as an NHS blue action button. For internal projects, this button is absent.
---
## Documents View (= Education & Certifications)
Education and certifications presented as attached documents in the patient record.
### Document List
```
+--[ Attached Documents ]-------------------------------------------------+
| Type | Document | Date | Source |
|----------------+----------------------------------+---------+------------|
| Certificate | MPharm (Hons) 2:1 | 2015 | UEA |
| Registration | GPhC Pharmacist Registration | 2016 | GPhC |
| Certificate | Mary Seacole Programme (78%) | 2018 | NHS LA |
| Results | A-Levels: Maths A*, Chem B, | 2011 | Highworth |
| | Politics C | | Grammar |
| Research | Drug Delivery & Cocrystals | 2015 | UEA |
| | (75.1% Distinction) | | |
+-------------------------------------------------------------------------+
```
### Document Type Icons
Small document icons from Lucide:
- `FileText` for certificates
- `Award` for registrations
- `GraduationCap` for academic results
- `FlaskConical` for research
### Expanded Document Preview
```
MPharm (Hons) 2:1 - University of East Anglia
|-- Type: Academic Qualification
|-- Date Awarded: 2015
|-- Institution: University of East Anglia, Norwich
|-- Classification: Upper Second-Class Honours (2:1)
|-- Duration: 2011 - 2015 (4 years)
|-- Research: Drug delivery and cocrystals
| Grade: 75.1% (Distinction)
|-- Notes: MPharm is a 4-year integrated Master's degree
required for pharmacist registration in the UK.
```
The preview panel uses the same tree-indented structure as the Investigations expanded view, maintaining visual consistency across the PMR interface.
+93
View File
@@ -0,0 +1,93 @@
# Reference: Medications View (= Skills)
> Extracted from goal.md — Medications View section. Skills presented as an active medications list.
---
## Overview
Skills presented as an active medications list — the format every pharmacist and GP reads daily.
## Full Table Layout
```
+--[ Active Medications ]-------------------------------------------------+
| Drug Name | Dose | Frequency | Start | Status |
|--------------------+-------+------------+----------+-------------------|
| Python | 90% | Daily | 2017 | Active (green) |
| SQL | 88% | Daily | 2017 | Active (green) |
| Power BI | 92% | Daily | 2019 | Active (green) |
| Data Analysis | 95% | Daily | 2016 | Active (green) |
| JavaScript / TS | 70% | Weekly | 2020 | Active (green) |
| Dashboard Dev | 88% | Weekly | 2019 | Active (green) |
| Algorithm Design | 82% | Weekly | 2022 | Active (green) |
| Data Pipelines | 80% | Weekly | 2022 | Active (green) |
+-------------------------------------------------------------------------+
+--[ Clinical Medications ]-----------------------------------------------+
| Drug Name | Dose | Frequency | Start | Status |
|-------------------------+-------+------------+--------+----------------|
| Medicines Optimisation | 95% | Daily | 2016 | Active (green) |
| Pop. Health Analytics | 90% | Daily | 2022 | Active (green) |
| NICE TA Implementation | 85% | Weekly | 2022 | Active (green) |
| Health Economics | 80% | Monthly | 2023 | Active (green) |
| Clinical Pathways | 82% | Weekly | 2022 | Active (green) |
| CD Assurance | 88% | Weekly | 2024 | Active (green) |
+-------------------------------------------------------------------------+
+--[ PRN (As Required) ]--------------------------------------------------+
| Drug Name | Dose | Frequency | Start | Status |
|-------------------------+-------+------------+--------+----------------|
| Budget Management | 90% | As needed | 2024 | Active (green) |
| Stakeholder Engagement | 88% | As needed | 2022 | Active (green) |
| Pharma Negotiation | 85% | As needed | 2024 | Active (green) |
| Team Development | 82% | As needed | 2017 | Active (green) |
+-------------------------------------------------------------------------+
```
## Column Definitions
| Column | PMR Meaning | CV Mapping |
|--------|------------|------------|
| Drug Name | Medication name | Skill name |
| Dose | Dosage strength | Proficiency percentage |
| Frequency | How often taken | How often the skill is used (Daily / Weekly / Monthly / As needed) |
| Start | Date prescribed | Year Andy started using this skill (approximate) |
| Status | Active / Stopped | Active (green dot) for current skills, Historical (gray dot) for deprecated skills |
## Medication Categories (tabs within the view)
Skills are grouped into three "medication types," mimicking how clinical systems separate regular, acute, and PRN medications:
- **Active Medications** = Technical skills (the "regular medications" — taken daily, core to function)
- **Clinical Medications** = Healthcare domain skills (the specialist prescriptions)
- **PRN (As Required)** = Strategic & leadership skills (used situationally, not daily)
## Table Styling
- Table headers: Inter 600, 13px, uppercase, gray-400, `#F9FAFB` background
- Table rows: alternating `#FFFFFF` / `#F9FAFB` backgrounds
- Row height: 40px
- All borders: `1px solid #E5E7EB`
- Hover state: row background changes to `#EFF6FF` (subtle blue tint)
- Status dots: 6px circles, inline with status text
## Interaction — Prescribing History
Clicking any medication/skill row expands it downward to show a "prescribing history" — a mini-timeline of how the skill developed:
```
Python | 90% | Daily | 2017 | Active (green)
|-- Prescribing History:
2017 Started: Self-taught for data analysis automation
2019 Increased: Dashboard development, data pipeline work
2022 Specialist use: Blueteq automation, Sankey analysis tools
2024 Advanced: Switching algorithm (14,000 patients), CD monitoring
2025 Current: Population-level analytics, incentive scheme automation
```
The history entries are styled in Geist Mono, 12px, with year markers as bold anchors and descriptions in regular weight. This "prescribing history" shows skill progression in a format that clinicians understand intuitively.
## Sortable Columns
Table columns are sortable by clicking the header. Clicking "Dose" sorts by proficiency descending. Clicking "Start" sorts chronologically. A small sort indicator arrow appears in the active sort column header. Default sort: by category grouping.
+60
View File
@@ -0,0 +1,60 @@
# Reference: Problems View (= Achievements / Challenges)
> Extracted from goal.md — Problems View section. Career achievements framed as clinical problems that were identified, treated, and resolved.
---
## Overview
The "Problems" list in a clinical record tracks diagnoses — conditions that were identified, treated, and either resolved or require ongoing management. This maps perfectly to career achievements: challenges that Andy identified and resolved.
## Two Sections: Active Problems and Resolved Problems
### Active Problems (current / ongoing)
```
+--[ Active Problems ]----------------------------------------------------+
| Status | Code | Problem | Since |
|--------+-----------+--------------------------------------+------------|
| AMB | [MGT001] | 220M prescribing budget oversight | Jul 2024 |
| GRN | [TRN001] | Patient-level SQL transformation | 2025 |
| AMB | [LEA001] | Team data literacy programme | Jul 2024 |
+-------------------------------------------------------------------------+
```
### Resolved Problems (past achievements)
```
+--[ Resolved Problems ]--------------------------------------------------+
| Status | Code | Problem | Resolved | Outcome |
|--------+-----------+--------------------------------+-----------+------------------------------------------|
| GRN | [EFF001] | Manual prescribing analysis | Oct 2025 | Python algorithm: 14,000 pts, 2.6M/yr |
| | | inefficiency | | |
| GRN | [EFF002] | 14.6M efficiency target | Oct 2025 | Over-target performance achieved |
| GRN | [AUT001] | Blueteq form creation backlog | 2023 | 70% reduction, 200hrs saved |
| GRN | [INN001] | Asthma screening scalability | 2019 | National rollout: ~300 branches, ~1M |
| GRN | [AUT002] | Incentive scheme manual calc. | 2025 | Automated: 50% Rx reduction in 2 months |
| GRN | [DAT001] | HCD spend tracking gaps | 2023 | Blueteq-secondary care data integration |
| GRN | [VIS001] | Patient pathway opacity | 2023 | Sankey chart analysis tool |
| GRN | [MON001] | Population opioid exposure | 2024 | CD monitoring system: OME tracking |
| | | monitoring | | |
+-------------------------------------------------------------------------+
```
## Column Definitions
| Column | Meaning |
|--------|---------|
| Status | Traffic light: Green (resolved), Amber (in progress / active), Red (urgent — unused, reserved) |
| Code | SNOMED-style reference code. Fictional but internally consistent. Formatted in Geist Mono. |
| Problem | The challenge or opportunity Andy identified |
| Resolved | Date or year the problem was resolved |
| Outcome | Brief description of the resolution and its measurable impact |
## Expandable Rows
Each problem row can be expanded to show a full narrative: what the problem was, how Andy approached it, what tools/methods were used, and the quantified outcome. The expanded state also shows "linked consultations" — clicking a link navigates to the relevant entry in Consultations view.
## Traffic Light Status Indicators
Traffic lights are 8px circles with the status colors (green, amber, red, gray). They appear inline before the code column. This is exactly how clinical systems indicate problem severity/status — it's an immediately scannable visual language.
+72
View File
@@ -0,0 +1,72 @@
# Reference: Referrals View (= Contact)
> Extracted from goal.md — Referrals View section. Contact information presented as a clinical referral form.
---
## Overview
Contact information presented as a clinical referral form — the mechanism for "referring" a patient (Andy) to another service.
## Referral Form Layout
```
+--[ New Referral ]-------------------------------------------------------+
| |
| Referring to: CHARLWOOD, Andrew (Mr) |
| NHS Number: 221 181 0 |
| |
| Priority: ( ) Urgent (*) Routine ( ) Two-Week Wait |
| |
| Referrer Name: [________________________] |
| Referrer Email: [________________________] |
| Referrer Org: [________________________] (optional) |
| |
| Reason for Referral: |
| [ ] |
| [ ] |
| [ ] |
| |
| Contact Method: ( ) Email ( ) Phone ( ) LinkedIn |
| |
| [ Cancel ] [ Send Referral ] |
+-------------------------------------------------------------------------+
```
## Priority Toggle (tongue-in-cheek)
Three radio options styled like clinical referral priorities:
- **Urgent**: Red label, red dot. Selectable but the tooltip reads "All enquiries are welcome, urgent or not."
- **Routine**: Blue label, blue dot. Default selected.
- **Two-Week Wait**: Amber label. Tooltip: "NHS cancer referral pathway - this isn't that, but the spirit of promptness applies."
This is the design's main moment of humor. The priority options are visually authentic to clinical referral forms, and the tongue-in-cheek tooltips reward exploration without undermining the professional tone.
## Form Fields
Standard clinical form inputs: `1px solid #D1D5DB` border, `4px` radius, `8px 12px` padding. Labels in Inter 500, 13px, gray-600, positioned above inputs. Focus state: border changes to NHS blue, subtle blue glow (`box-shadow: 0 0 0 3px rgba(0, 94, 184, 0.15)`).
## Submit Button
"Send Referral" in NHS blue (`#005EB8`), white text, full width of the right half of the form. On hover: darkens to `#004494`. On click: brief loading state (spinner icon), then a success message:
```
Checkmark Referral sent successfully
Reference: REF-2026-0210-001
Expected response time: 24-48 hours
```
The reference number is generated from the current date. The success state mimics the confirmation screen shown after submitting a clinical referral in EMIS.
## Alternative Contact Methods (below the form)
```
+--[ Direct Contact ]-----------------------------------------------------+
| Email: andy@charlwood.xyz [Send Email ->] |
| Phone: 07795553088 [Call ->] |
| LinkedIn: linkedin.com/in/andycharlwood [View Profile ->] |
| Location: Norwich, UK |
+-------------------------------------------------------------------------+
```
Styled as a simple key-value table, same format as the Patient Demographics card in Summary view.
+116
View File
@@ -0,0 +1,116 @@
# Reference: Summary View + Clinical Alert
> Extracted from goal.md — Summary View and Clinical Alert sections. This is the landing view after login.
---
## Summary View
The landing view after login. This mimics the "Patient Summary" screen — the first screen a clinician sees when opening a patient record, showing the most important information at a glance.
**Layout:** A grid of summary cards arranged in a 2-column layout on desktop, single column on mobile. Each card has a header bar with the card title in Inter 600, 14px, uppercase, on a `#F9FAFB` background with `1px solid #E5E7EB` bottom border.
### Card 1: Patient Demographics (spans full width)
```
+--[ Patient Demographics ]------------------------------------------+
| Name: Andrew Charlwood Status: Active (dot) |
| DOB: 14 February 1993 Location: Norwich, UK |
| Registration: GPhC 2211810 Since: August 2016 |
| Qualification: MPharm (Hons) 2:1 University: UEA, 2015 |
+---------------------------------------------------------------------+
```
A two-column key-value table. Labels in Inter 500, 13px, gray-500. Values in Inter 400, 14px, gray-900. Labels right-aligned, values left-aligned — mimicking clinical system demographics layout.
### Card 2: Active Problems (left column)
```
+--[ Active Problems ]-----------------------------------------------+
| (green dot) Deputy Head, Pop. Health & Data Analysis Jul 2024-Present |
| NHS Norfolk & Waveney ICB |
| (green dot) 220M prescribing budget management Ongoing |
| (amber dot) Patient-level SQL analytics transformation In progress |
+---------------------------------------------------------------------+
```
A list with green dots for active/current items, amber dots for in-progress items. Each entry has a title in Inter 500, 14px, and a date range or status in Geist Mono, 12px, right-aligned. Click an entry to navigate to the corresponding Consultation.
### Card 3: Current Medications — Quick View (right column)
```
+--[ Current Medications (Quick View) ]-------------------------------+
| Python | 90% | Daily | Active (green dot) |
| SQL | 88% | Daily | Active (green dot) |
| Power BI | 92% | Daily | Active (green dot) |
| Data Analysis | 95% | Daily | Active (green dot) |
| JS / TypeScript | 70% | Weekly | Active (green dot) |
| [View Full List ->] |
+---------------------------------------------------------------------+
```
A compact 4-column table showing the top 5 skills. "View Full List" links to the Medications view. Table headers are uppercase, 12px, gray-400. Table rows alternate between `#FFFFFF` and `#F9FAFB` backgrounds.
### Card 4: Last Consultation (spans full width)
```
+--[ Last Consultation ]----------------------------------------------+
| Date: May 2025 Clinician: A. Charlwood Location: NHS N&W ICB |
| |
| Interim Head, Population Health & Data Analysis |
| Led strategic delivery of population health initiatives and |
| data-driven medicines optimisation across Norfolk & Waveney ICS... |
| [View Full Record ->] |
+---------------------------------------------------------------------+
```
A preview of the most recent role, truncated to 2-3 lines. "View Full Record" navigates to Consultations with that entry expanded.
### Card 5: Alerts (full width, positioned above all other cards)
This is the Clinical Alert — see below.
---
## The Clinical Alert (Signature Interaction)
When the user first loads the Summary view (immediately after the login transition), a clinical alert banner slides down from beneath the patient banner.
### Alert Styling
```
+--[ WARNING CLINICAL ALERT ]------------------------------------------+
| WARNING ALERT: This patient has identified 14.6M in prescribing |
| efficiency savings across Norfolk & Waveney ICS. |
| [Acknowledge]|
+----------------------------------------------------------------------+
```
- Background: amber (`#FEF3C7` — amber-100, light amber)
- Left border: 4px solid `#F59E0B` (amber-500)
- Warning icon: `AlertTriangle` from Lucide, amber-600
- Text: Inter 500, 14px, `#92400E` (amber-800)
- "Acknowledge" button: small outlined button, amber border and text
### Behavior
1. The alert slides down from beneath the patient banner with a spring animation (250ms, slight overshoot) after the PMR interface finishes materializing.
2. It pushes the Summary content downward, so it's impossible to miss.
3. Clicking "Acknowledge" triggers a brief animation: a green checkmark replaces the warning icon (200ms), then the alert collapses upward (200ms, ease-out) and is gone.
4. The dismiss state is stored in React state (session-only) — refreshing the page shows the alert again.
### Why This Works
Clinical alerts are the mechanism that clinical systems use to put critical information in front of clinicians before they do anything else. They are the highest-priority information in the system. By framing Andy's most impressive metric ("14.6M") as a clinical alert, it gets the same treatment — it's the first thing the user reads, it demands acknowledgment, and its format gives the number institutional weight. This is not a boast in a paragraph; it's a system-generated alert based on data. The framing makes the achievement feel objective.
### Second Alert (on Consultations view)
When the user first navigates to Consultations, a secondary alert appears:
```
WARNING NOTE: Patient has developed a Python-based switching algorithm
identifying 14,000 patients for cost-effective medication alternatives.
2.6M annual savings potential. Review recommended.
```
This second alert reinforces the key technical achievement in clinical language. It appears only once (on first navigation to Consultations) and is dismissible with the same "Acknowledge" interaction.
+202
View File
@@ -0,0 +1,202 @@
# Reference: ECG Transition + Login Sequence
> Extracted from goal.md — ECG Transition section. This covers the flatline exit from the ECG animation and the immersive login sequence that bridges into the PMR interface.
---
## Starting Point
"ANDREW CHARLWOOD" is on screen in neon green (`#00ff41`) on black. The heartbeat trace is complete. The name is fully formed and glowing.
## Phase 1: The Flatline (600ms)
The neon green name holds for a beat (300ms). Then the glow around the letters begins to fade. Simultaneously, from the right edge of the name, a flatline trace extends rightward — a perfectly horizontal green line drawn at the baseline, extending across the remaining viewport width over 300ms. The visual reads as a patient monitor flatline. This is deliberate: the "patient" (the animation phase) is ending. A new record is about to open.
The flatline has a subtle audio-visual implication without actual sound — the green line is steady and unbroken, the glow around the name letters reduces to zero. The entire canvas is now: a fading green name with a horizontal flatline extending to the right edge. All on black.
## Phase 2: Screen Clear (400ms)
The entire canvas fades to black over 200ms (the name and flatline dissolve into darkness). Then, from black, the background transitions to a dark blue-gray (`#1E293B`) over 200ms. This is the color of a clinical system login screen — the dark institutional background that every NHS worker recognizes from their Monday morning.
## Phase 3: Login Sequence (1200ms)
A login panel materializes center-screen: a white card (320px wide, 12px border-radius, subtle shadow) on the dark blue-gray background. The card contains:
- A small NHS-blue shield icon or generic clinical system logo at the top
- **Username field**: Empty text input with label "Username". After 200ms, a cursor appears and types `A.CHARLWOOD` character by character (30ms per character, ~350ms total). The typing uses Geist Mono / monospace font.
- **Password field**: After a 150ms pause, dots fill the password field in rapid succession (8 dots, 20ms each, ~160ms total).
- **"Log In" button**: NHS blue (`#005EB8`), full width. After another 150ms pause, the button receives a subtle pressed state (darkens slightly, 100ms) as if clicked.
The login card holds for 200ms in its "submitted" state, then...
## Phase 4: Interface Materialization (500ms)
The login card scales up slightly (103%) and fades out (200ms). As it fades, the full PMR interface fades in behind it:
1. **Patient banner** slides down from the top edge (200ms, ease-out)
2. **Sidebar** slides in from the left edge (250ms, ease-out, starting 50ms after the banner)
3. **Main content area** (Summary view) fades in (300ms, starting 100ms after sidebar begins)
4. **Clinical alert banner** slides down from beneath the patient banner (250ms, spring easing, starting 200ms after main content appears)
## Phase 5: Final State
The full PMR interface is visible: patient banner at top, dark sidebar on left, Summary view in the main content area, and the clinical alert banner demanding attention. The user is now "logged in" to Andy's career record.
**Total transition duration:** ~2.7 seconds
## Why This Works
The login sequence is the most immersive transition of all designs. Every NHS worker, every pharmacist, every GP has typed their credentials into a clinical system at 8am on a Monday. This transition puts them right there. It's specific, it's authentic, and it immediately establishes the metaphor: you are opening a patient record. The "patient" happens to be a career.
## Login Animation Implementation Notes
- Component mounts with dark blue-gray background
- Login card fades in (Framer Motion, 200ms)
- Username typing: `setInterval` adds one character per 30ms to a state string
- Password dots: `setInterval` adds one dot per 20ms
- Button press: state change triggers visual pressed state, then 200ms delay
- `onComplete` callback fires, parent component swaps to PMRInterface
- Typing respects `prefers-reduced-motion` — with reduced motion, full username appears instantly and login completes in ~500ms total
- **Font: Geist Mono** for username/password fields (NOT Fira Code)
---
## Design Guidance (from /frontend-design)
> Pre-baked design direction. Do NOT invoke `/frontend-design` at runtime — this section contains the output.
### Aesthetic Direction: Institutional Utilitarian
This is not "exciting" design — it is the visual equivalent of fluorescent lights, laminate desks, and the smell of hand sanitiser at 07:58 on a Monday morning. The card must look like every single hospital login prompt a doctor has ever seen: clean white, unadorned, functional. No personality. The branding is the only concession to identity. The magic is not visual flair — it is the uncanny recognition of "oh, this is exactly what that looks like" combined with the satisfying typewriter rhythm of credentials appearing.
### Key Design Decisions
1. **Active field focus ring**: NHS-blue border (`1px solid #005EB8`) on the currently active field, inactive fields shift to `#FAFAFA` background. Mirrors real NHS login forms (Lorenzo, SystmOne, EMIS Web). Transition 150ms.
2. **Reduced shadow to spec**: Use exactly `0 1px 2px rgba(0,0,0,0.03)`. Card sits on dark background through border definition, not shadow depth — more faithful to real NHS software.
3. **Border**: Use `1px solid #E5E7EB` per design system (not `rgba(255,255,255,0.1)`).
4. **Timer cleanup**: Track every `setInterval` and `setTimeout` via refs, clear all on unmount.
5. **Consolidated active field state**: Single `activeField` state (`'username' | 'password' | null`) instead of separate booleans.
6. **Accessibility**: `role="status"` + `aria-label` on outer container. Cursor pipes `aria-hidden="true"`. Card entrance `scale: 0.98` (not 0).
7. **The Monday-morning feeling**: No gradients, no decorative elements, no loading spinners, no "Welcome back!" messaging. Just white rectangle on gray, shield icon, two fields, button. Typing speed deliberately mechanical.
### Implementation Pattern
```tsx
import { useState, useEffect, useCallback, useRef } from 'react'
import { motion } from 'framer-motion'
import { Shield } from 'lucide-react'
interface LoginScreenProps {
onComplete: () => void
}
// Key state
const [username, setUsername] = useState('')
const [passwordDots, setPasswordDots] = useState(0)
const [showCursor, setShowCursor] = useState(true)
const [activeField, setActiveField] = useState<'username' | 'password' | null>('username')
const [buttonPressed, setButtonPressed] = useState(false)
const [isExiting, setIsExiting] = useState(false)
const fullUsername = 'A.CHARLWOOD'
const passwordLength = 8
```
Card structure:
```tsx
<motion.div
className="bg-white"
style={{
width: '320px',
padding: '32px',
borderRadius: '12px',
border: '1px solid #E5E7EB',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)',
}}
initial={{ opacity: 0, scale: 0.98 }}
animate={isExiting ? { scale: 1.03, opacity: 0 } : { scale: 1, opacity: 1 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
```
Branding header:
```tsx
<div className="flex flex-col items-center" style={{ marginBottom: '28px' }}>
<div style={{
padding: '10px',
borderRadius: '8px',
backgroundColor: 'rgba(0, 94, 184, 0.07)',
marginBottom: '10px',
}}>
<Shield size={26} style={{ color: '#005EB8' }} strokeWidth={2.5} />
</div>
<span style={{
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: '13px', fontWeight: 600,
color: '#64748B', letterSpacing: '0.01em',
}}>CareerRecord PMR</span>
<span style={{
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: '11px', fontWeight: 400,
color: '#94A3B8', marginTop: '2px',
}}>Clinical Information System</span>
</div>
```
Input field pattern (username example):
```tsx
<div style={{
width: '100%',
padding: '9px 11px',
fontFamily: "'Geist Mono', 'Fira Code', monospace",
fontSize: '13px',
backgroundColor: activeField === 'username' ? '#FFFFFF' : '#FAFAFA',
border: activeField === 'username' ? '1px solid #005EB8' : '1px solid #E5E7EB',
borderRadius: '4px',
color: '#111827',
minHeight: '38px',
display: 'flex',
alignItems: 'center',
}}>
<span>{username}</span>
{activeField === 'username' && (
<span style={{ opacity: showCursor ? 1 : 0, color: '#005EB8' }} aria-hidden="true">|</span>
)}
</div>
```
Login button:
```tsx
<button style={{
width: '100%',
padding: '10px 16px',
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: '14px', fontWeight: 600,
color: '#FFFFFF',
backgroundColor: buttonPressed ? '#004494' : '#005EB8',
border: 'none',
borderRadius: '4px',
}}>Log In</button>
```
Typing sequence (reduced motion branch):
```tsx
if (prefersReducedMotion) {
setUsername(fullUsername)
setPasswordDots(passwordLength)
setActiveField(null)
setTimeout(() => { setButtonPressed(true); setTimeout(triggerComplete, 100) }, 300)
return
}
// Normal: username at 30ms/char, 150ms pause, password at 20ms/dot, 150ms pause, button press
```
Footer:
```tsx
<div style={{ marginTop: '22px', paddingTop: '18px', borderTop: '1px solid #E5E7EB' }}>
<p style={{
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: '11px', color: '#94A3B8', textAlign: 'center',
}}>Secure clinical system login</p>
</div>
```
+1 -1
View File
@@ -11,7 +11,7 @@ function App() {
return ( return (
<AccessibilityProvider> <AccessibilityProvider>
<div className="min-h-screen"> <div className="min-h-screen bg-black">
{phase === 'boot' && ( {phase === 'boot' && (
<BootSequence onComplete={() => setPhase('ecg')} /> <BootSequence onComplete={() => setPhase('ecg')} />
)} )}
+22 -20
View File
@@ -7,6 +7,7 @@ interface BootLine {
} }
const bootLines: BootLine[] = [ const bootLines: BootLine[] = [
{ html: '<span class="text-[#00ff41] font-bold">CLINICAL TERMINAL v3.2.1</span>', delay: 0 }, { html: '<span class="text-[#00ff41] font-bold">CLINICAL TERMINAL v3.2.1</span>', delay: 0 },
{ html: '<span class="text-[#3a6b45]">Initialising pharmacist profile...</span>', delay: 220 }, { html: '<span class="text-[#3a6b45]">Initialising pharmacist profile...</span>', delay: 220 },
{ html: '<span class="text-[#3a6b45]">---</span>', delay: 220 }, { html: '<span class="text-[#3a6b45]">---</span>', delay: 220 },
@@ -23,34 +24,35 @@ const bootLines: BootLine[] = [
{ html: '<span class="text-[#00ff41] font-bold">&gt; READY — Rendering CV..<span class="ecg-seed-dot" id="ecg-seed-dot">.</span></span>', delay: 220 }, { html: '<span class="text-[#00ff41] font-bold">&gt; READY — Rendering CV..<span class="ecg-seed-dot" id="ecg-seed-dot">.</span></span>', delay: 220 },
] ]
// Precompute cumulative delays so the first render can use them
const bootLineDelays: number[] = (() => {
const delays: number[] = []
let total = 0
bootLines.forEach((line) => {
delays.push(total)
total += line.delay
})
return delays
})()
interface BootSequenceProps { interface BootSequenceProps {
onComplete: () => void onComplete: () => void
} }
export function BootSequence({ onComplete }: BootSequenceProps) { export function BootSequence({ onComplete }: BootSequenceProps) {
const [isVisible, setIsVisible] = useState(true) const [isVisible, setIsVisible] = useState(true)
const [lineDelays, setLineDelays] = useState<number[]>([])
useEffect(() => { useEffect(() => {
const delays: number[] = [] const totalBootTime = bootLines.reduce((sum, l) => sum + l.delay, 0)
let totalDelay = 0
bootLines.forEach((line) => {
delays.push(totalDelay)
totalDelay += line.delay
})
setLineDelays(delays)
const totalBootTime = totalDelay
const fadeStartTime = totalBootTime + 400 const fadeStartTime = totalBootTime + 400
const fadeTimer = setTimeout(() => { const fadeTimer = setTimeout(() => {
setIsVisible(false) setIsVisible(false)
}, fadeStartTime) }, fadeStartTime)
const completeTimer = setTimeout(() => { const completeTimer = setTimeout(() => {
onComplete() onComplete()
}, fadeStartTime + 800) }, fadeStartTime+2000)
return () => { return () => {
clearTimeout(fadeTimer) clearTimeout(fadeTimer)
clearTimeout(completeTimer) clearTimeout(completeTimer)
@@ -63,10 +65,10 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
<motion.div <motion.div
className="fixed inset-0 z-50 flex flex-col justify-center bg-black p-10 font-mono text-sm overflow-hidden" className="fixed inset-0 z-50 flex flex-col justify-center bg-black p-10 font-mono text-sm overflow-hidden"
initial={{ opacity: 1 }} initial={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 1 }}
transition={{ duration: 0.8, ease: 'easeOut' }} transition={{ delay: 2, duration: 0.8, ease: 'easeOut' }}
> >
<div className="flex flex-col gap-1 max-w-[640px]"> <div className="flex flex-col gap-1 max-w-[640px] transform -translate-y-1/2">
{bootLines.map((line, index) => ( {bootLines.map((line, index) => (
<motion.div <motion.div
key={index} key={index}
@@ -74,7 +76,7 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
initial={{ opacity: 0, y: 8 }} initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
delay: lineDelays[index] / 1000, delay: (bootLineDelays[index] ?? 0) / 1000,
duration: 0.4, duration: 0.4,
ease: 'easeOut', ease: 'easeOut',
}} }}
@@ -85,7 +87,7 @@ export function BootSequence({ onComplete }: BootSequenceProps) {
className="inline-block w-2 h-4 bg-[#00ff41] ml-1 animate-blink" className="inline-block w-2 h-4 bg-[#00ff41] ml-1 animate-blink"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: lineDelays[lineDelays.length - 1] / 1000 }} transition={{ delay: 2 + (bootLineDelays[bootLineDelays.length + 1] ?? 0) / 1000 }}
/> />
</div> </div>
</motion.div> </motion.div>
+115 -46
View File
@@ -91,62 +91,96 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
}, [startLoginSequence]) }, [startLoginSequence])
return ( return (
<div <div
className="fixed inset-0 flex items-center justify-center z-50" className="fixed inset-0 flex items-center justify-center z-50"
style={{ backgroundColor: '#1E293B' }} style={{ backgroundColor: '#1E293B' }}
> >
<motion.div <motion.div
className="bg-white rounded-xl shadow-lg p-8" className="bg-white p-8"
style={{ style={{
width: '320px', width: '320px',
borderRadius: '12px', borderRadius: '12px',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.3)', border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15), 0 1px 3px rgba(0, 0, 0, 0.1)',
}} }}
initial={{ opacity: 0 }}
animate={isExiting ? { scale: 1.03, opacity: 0 } : { scale: 1, opacity: 1 }} animate={isExiting ? { scale: 1.03, opacity: 0 } : { scale: 1, opacity: 1 }}
transition={{ duration: 0.2, ease: 'easeOut' }} transition={{ duration: 0.2, ease: 'easeOut' }}
> >
<div className="flex flex-col items-center mb-6"> {/* Branding */}
<div <div className="flex flex-col items-center mb-8">
className="p-3 rounded-lg mb-4" <div
style={{ backgroundColor: 'rgba(0, 94, 184, 0.1)' }} className="p-3 rounded-lg mb-3"
style={{ backgroundColor: 'rgba(0, 94, 184, 0.08)' }}
> >
<Shield <Shield
size={32} size={28}
style={{ color: '#005EB8' }} style={{ color: '#005EB8' }}
strokeWidth={2} strokeWidth={2.5}
/> />
</div> </div>
<span <span
className="text-sm font-medium" style={{
style={{ color: '#6B7280' }} fontFamily: 'Inter, sans-serif',
fontSize: '13px',
fontWeight: 600,
color: '#64748B',
letterSpacing: '0.01em',
}}
> >
CareerRecord PMR CareerRecord PMR
</span> </span>
<span
style={{
fontFamily: 'Inter, sans-serif',
fontSize: '11px',
fontWeight: 400,
color: '#94A3B8',
marginTop: '2px',
}}
>
Clinical Information System
</span>
</div> </div>
<div className="space-y-4"> {/* Login Form */}
<div className="space-y-5">
{/* Username Field */}
<div> <div>
<label <label
className="block text-xs font-medium mb-1.5" style={{
style={{ color: '#6B7280' }} display: 'block',
fontFamily: 'Inter, sans-serif',
fontSize: '12px',
fontWeight: 500,
color: '#64748B',
marginBottom: '6px',
}}
> >
Username Username
</label> </label>
<div <div
className="w-full px-3 py-2.5 rounded text-sm" style={{
style={{ width: '100%',
fontFamily: "'Fira Code', monospace", padding: '10px 12px',
backgroundColor: '#F9FAFB', fontFamily: "'Geist Mono', 'Courier New', monospace",
border: '1px solid #E5E7EB', fontSize: '13px',
backgroundColor: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '4px',
color: '#111827', color: '#111827',
minHeight: '38px',
display: 'flex',
alignItems: 'center',
}} }}
> >
<span>{username}</span> <span>{username}</span>
{isTypingUsername && ( {isTypingUsername && (
<span <span
style={{ style={{
opacity: showCursor ? 1 : 0, opacity: showCursor ? 1 : 0,
color: '#005EB8', color: '#005EB8',
marginLeft: '1px',
}} }}
> >
| |
@@ -155,29 +189,43 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
</div> </div>
</div> </div>
{/* Password Field */}
<div> <div>
<label <label
className="block text-xs font-medium mb-1.5" style={{
style={{ color: '#6B7280' }} display: 'block',
fontFamily: 'Inter, sans-serif',
fontSize: '12px',
fontWeight: 500,
color: '#64748B',
marginBottom: '6px',
}}
> >
Password Password
</label> </label>
<div <div
className="w-full px-3 py-2.5 rounded text-sm" style={{
style={{ width: '100%',
fontFamily: "'Fira Code', monospace", padding: '10px 12px',
backgroundColor: '#F9FAFB', fontFamily: "'Geist Mono', 'Courier New', monospace",
border: '1px solid #E5E7EB', fontSize: '13px',
backgroundColor: '#FFFFFF',
border: '1px solid #D1D5DB',
borderRadius: '4px',
color: '#111827', color: '#111827',
letterSpacing: '0.1em', letterSpacing: '0.15em',
minHeight: '38px',
display: 'flex',
alignItems: 'center',
}} }}
> >
<span>{'\u2022'.repeat(passwordDots)}</span> <span>{'\u2022'.repeat(passwordDots)}</span>
{isTypingPassword && ( {isTypingPassword && (
<span <span
style={{ style={{
opacity: showCursor ? 1 : 0, opacity: showCursor ? 1 : 0,
color: '#005EB8', color: '#005EB8',
marginLeft: '2px',
}} }}
> >
| |
@@ -186,22 +234,43 @@ export function LoginScreen({ onComplete }: LoginScreenProps) {
</div> </div>
</div> </div>
{/* Log In Button */}
<button <button
className="w-full py-2.5 rounded text-sm font-semibold text-white transition-all duration-100" style={{
style={{ width: '100%',
padding: '11px 16px',
fontFamily: 'Inter, sans-serif',
fontSize: '14px',
fontWeight: 600,
color: '#FFFFFF',
backgroundColor: buttonPressed ? '#004494' : '#005EB8', backgroundColor: buttonPressed ? '#004494' : '#005EB8',
border: 'none',
borderRadius: '4px', borderRadius: '4px',
transform: buttonPressed ? 'scale(0.98)' : 'scale(1)', cursor: 'pointer',
transition: 'background-color 100ms ease-out',
marginTop: '8px',
}} }}
> >
Log In Log In
</button> </button>
</div> </div>
<div className="mt-6 pt-4 border-t border-gray-100"> {/* Footer */}
<p <div
className="text-xs text-center" style={{
style={{ color: '#9CA3AF' }} marginTop: '24px',
paddingTop: '20px',
borderTop: '1px solid #E5E7EB',
}}
>
<p
style={{
fontFamily: 'Inter, sans-serif',
fontSize: '11px',
color: '#94A3B8',
textAlign: 'center',
lineHeight: '1.4',
}}
> >
Secure clinical system login Secure clinical system login
</p> </p>