Compare commits
38 Commits
cfd0283c78
...
e13a073a6f
| Author | SHA1 | Date | |
|---|---|---|---|
| e13a073a6f | |||
| 000df670a3 | |||
| b9db2f5401 | |||
| c3316b9c45 | |||
| b3ebff26bf | |||
| 85ac1b879f | |||
| 4db3be0abb | |||
| f96c6a99d1 | |||
| 7461a83b9d | |||
| b480b742c8 | |||
| bfd17a3e80 | |||
| bba61f73b6 | |||
| 8765470627 | |||
| 43aa836317 | |||
| f0cb6b924f | |||
| 06f0d658b0 | |||
| ad1ce81948 | |||
| 2be346144c | |||
| 1d8cb78143 | |||
| cd4aa1e240 | |||
| fd9dd7d00e | |||
| 8f6bfd0b5e | |||
| 803c4f8a48 | |||
| 5533cded82 | |||
| 86e0015393 | |||
| d16656b954 | |||
| b7471c5cf8 | |||
| 5579e2741a | |||
| f75a6b9a5f | |||
| 8094f74800 | |||
| 4324f06186 | |||
| 5e1c96edfa | |||
| 556940c3c8 | |||
| b8c1aedb5a | |||
| 5a000d6457 | |||
| 3afadbdc73 | |||
| 4eeeb05744 | |||
| 959f0e1842 |
@@ -8,7 +8,27 @@
|
||||
"Bash(start \"\" \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\cv-4-vitals-monitor\\\\4-vitals-monitor.html\")",
|
||||
"Bash(npx skills find:*)",
|
||||
"WebSearch",
|
||||
"Bash(ls \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\New CV website\\\\designs\"\" 2>nul || echo \"Directory does not exist \")"
|
||||
"Bash(ls \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\New CV website\\\\designs\"\" 2>nul || echo \"Directory does not exist \")",
|
||||
"Bash(npm run typecheck:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(dir:*)",
|
||||
"mcp__playwright__browser_snapshot",
|
||||
"mcp__playwright__browser_navigate",
|
||||
"mcp__playwright__browser_take_screenshot",
|
||||
"Bash(npm run lint:*)",
|
||||
"Bash(curl:*)",
|
||||
"mcp__playwright__browser_click",
|
||||
"mcp__playwright__browser_wait_for",
|
||||
"mcp__playwright__browser_evaluate",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nTask 4: Rebuild PatientBanner with premium fonts, tooltip, and animations\n\n- Replace font-inter with font-ui \\(Elvaro Grotesque\\) throughout banner\n- Add custom NHSNumberWithTooltip with Framer Motion animated reveal\n- Add AnimatePresence crossfade between full/condensed banner states\n- Animate mobile overflow menu enter/exit\n- Add SkipButton to App.tsx for boot/ECG phase skip\n- Add shadow-pmr-banner, focus ring styles, prefers-reduced-motion support\n- Fix mobile banner to use patient data instead of hardcoded values\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(npx -y serve -l 3333 .)",
|
||||
"Bash(npx serve:*)",
|
||||
"Bash(timeout /t 3 /nobreak)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,3 +25,8 @@ dist-ssr
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
#Playwrite Screenshots
|
||||
*.png
|
||||
|
||||
nul
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
var chalk = require('chalk'),
|
||||
os = require('os'),
|
||||
httpServer = require('../lib/http-server'),
|
||||
portfinder = require('portfinder'),
|
||||
opener = require('opener'),
|
||||
|
||||
fs = require('fs'),
|
||||
url = require('url');
|
||||
var argv = require('minimist')(process.argv.slice(2), {
|
||||
alias: {
|
||||
tls: 'ssl'
|
||||
}
|
||||
});
|
||||
var ifaces = os.networkInterfaces();
|
||||
|
||||
process.title = 'http-server';
|
||||
|
||||
if (argv.h || argv.help) {
|
||||
console.log([
|
||||
'usage: http-server [path] [options]',
|
||||
'',
|
||||
'options:',
|
||||
' -p --port Port to use. If 0, look for open port. [8080]',
|
||||
' -a Address to use [0.0.0.0]',
|
||||
' -d Show directory listings [true]',
|
||||
' -i Display autoIndex [true]',
|
||||
' -g --gzip Serve gzip files when possible [false]',
|
||||
' -b --brotli Serve brotli files when possible [false]',
|
||||
' If both brotli and gzip are enabled, brotli takes precedence',
|
||||
' -e --ext Default file extension if none supplied [none]',
|
||||
' -s --silent Suppress log messages from output',
|
||||
' --cors[=headers] Enable CORS via the "Access-Control-Allow-Origin" header',
|
||||
' Optionally provide CORS headers list separated by commas',
|
||||
' -o [path] Open browser window after starting the server.',
|
||||
' Optionally provide a URL path to open the browser window to.',
|
||||
' -c Cache time (max-age) in seconds [3600], e.g. -c10 for 10 seconds.',
|
||||
' To disable caching, use -c-1.',
|
||||
' -t Connections timeout in seconds [120], e.g. -t60 for 1 minute.',
|
||||
' To disable timeout, use -t0',
|
||||
' -U --utc Use UTC time format in log messages.',
|
||||
' --log-ip Enable logging of the client\'s IP address',
|
||||
'',
|
||||
' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com',
|
||||
' --proxy-options Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false',
|
||||
'',
|
||||
' --username Username for basic authentication [none]',
|
||||
' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME',
|
||||
' --password Password for basic authentication [none]',
|
||||
' Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD',
|
||||
'',
|
||||
' -S --tls --ssl Enable secure request serving with TLS/SSL (HTTPS)',
|
||||
' -C --cert Path to TLS cert file (default: cert.pem)',
|
||||
' -K --key Path to TLS key file (default: key.pem)',
|
||||
'',
|
||||
' -r --robots Respond to /robots.txt [User-agent: *\\nDisallow: /]',
|
||||
' --no-dotfiles Do not show dotfiles',
|
||||
' --mimetypes Path to a .types file for custom mimetype definition',
|
||||
' -h --help Print this list and exit.',
|
||||
' -v --version Print the version and exit.'
|
||||
].join('\n'));
|
||||
process.exit();
|
||||
}
|
||||
|
||||
var port = argv.p || argv.port || parseInt(process.env.PORT, 10),
|
||||
host = argv.a || '0.0.0.0',
|
||||
tls = argv.S || argv.tls,
|
||||
sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE,
|
||||
proxy = argv.P || argv.proxy,
|
||||
proxyOptions = argv['proxy-options'],
|
||||
utc = argv.U || argv.utc,
|
||||
version = argv.v || argv.version,
|
||||
logger;
|
||||
|
||||
var proxyOptionsBooleanProps = [
|
||||
'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath', 'changeOrigin',
|
||||
'preserveHeaderKeyCase', 'followRedirects', 'selfHandleResponse'
|
||||
];
|
||||
|
||||
if (proxyOptions) {
|
||||
Object.keys(proxyOptions).forEach(function (key) {
|
||||
if (proxyOptionsBooleanProps.indexOf(key) > -1) {
|
||||
proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!argv.s && !argv.silent) {
|
||||
logger = {
|
||||
info: console.log,
|
||||
request: function (req, res, error) {
|
||||
var date = utc ? new Date().toUTCString() : new Date();
|
||||
var ip = argv['log-ip']
|
||||
? req.headers['x-forwarded-for'] || '' + req.connection.remoteAddress
|
||||
: '';
|
||||
if (error) {
|
||||
logger.info(
|
||||
'[%s] %s "%s %s" Error (%s): "%s"',
|
||||
date, ip, chalk.red(req.method), chalk.red(req.url),
|
||||
chalk.red(error.status.toString()), chalk.red(error.message)
|
||||
);
|
||||
}
|
||||
else {
|
||||
logger.info(
|
||||
'[%s] %s "%s %s" "%s"',
|
||||
date, ip, chalk.cyan(req.method), chalk.cyan(req.url),
|
||||
req.headers['user-agent']
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (chalk) {
|
||||
logger = {
|
||||
info: function () {},
|
||||
request: function () {}
|
||||
};
|
||||
}
|
||||
|
||||
if (version) {
|
||||
logger.info('v' + require('../package.json').version);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
portfinder.basePort = 8080;
|
||||
portfinder.getPort(function (err, port) {
|
||||
if (err) { throw err; }
|
||||
listen(port);
|
||||
});
|
||||
}
|
||||
else {
|
||||
listen(port);
|
||||
}
|
||||
|
||||
function listen(port) {
|
||||
var options = {
|
||||
root: argv._[0],
|
||||
cache: argv.c,
|
||||
timeout: argv.t,
|
||||
showDir: argv.d,
|
||||
autoIndex: argv.i,
|
||||
gzip: argv.g || argv.gzip,
|
||||
brotli: argv.b || argv.brotli,
|
||||
robots: argv.r || argv.robots,
|
||||
ext: argv.e || argv.ext,
|
||||
logFn: logger.request,
|
||||
proxy: proxy,
|
||||
proxyOptions: proxyOptions,
|
||||
showDotfiles: argv.dotfiles,
|
||||
mimetypes: argv.mimetypes,
|
||||
username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME,
|
||||
password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD
|
||||
};
|
||||
|
||||
if (argv.cors) {
|
||||
options.cors = true;
|
||||
if (typeof argv.cors === 'string') {
|
||||
options.corsHeaders = argv.cors;
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
try {
|
||||
new url.URL(proxy)
|
||||
}
|
||||
catch (err) {
|
||||
logger.info(chalk.red('Error: Invalid proxy url'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (tls) {
|
||||
options.https = {
|
||||
cert: argv.C || argv.cert || 'cert.pem',
|
||||
key: argv.K || argv.key || 'key.pem',
|
||||
passphrase: sslPassphrase,
|
||||
};
|
||||
try {
|
||||
fs.lstatSync(options.https.cert);
|
||||
}
|
||||
catch (err) {
|
||||
logger.info(chalk.red('Error: Could not find certificate ' + options.https.cert));
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
fs.lstatSync(options.https.key);
|
||||
}
|
||||
catch (err) {
|
||||
logger.info(chalk.red('Error: Could not find private key ' + options.https.key));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
var server = httpServer.createServer(options);
|
||||
server.listen(port, host, function () {
|
||||
var protocol = tls ? 'https://' : 'http://';
|
||||
|
||||
logger.info([
|
||||
chalk.yellow('Starting up http-server, serving '),
|
||||
chalk.cyan(server.root),
|
||||
tls ? (chalk.yellow(' through') + chalk.cyan(' https')) : ''
|
||||
].join(''));
|
||||
|
||||
logger.info([chalk.yellow('\nhttp-server version: '), chalk.cyan(require('../package.json').version)].join(''));
|
||||
|
||||
logger.info([
|
||||
chalk.yellow('\nhttp-server settings: '),
|
||||
([chalk.yellow('CORS: '), argv.cors ? chalk.cyan(argv.cors) : chalk.red('disabled')].join('')),
|
||||
([chalk.yellow('Cache: '), argv.c ? (argv.c === '-1' ? chalk.red('disabled') : chalk.cyan(argv.c + ' seconds')) : chalk.cyan('3600 seconds')].join('')),
|
||||
([chalk.yellow('Connection Timeout: '), argv.t === '0' ? chalk.red('disabled') : (argv.t ? chalk.cyan(argv.t + ' seconds') : chalk.cyan('120 seconds'))].join('')),
|
||||
([chalk.yellow('Directory Listings: '), argv.d ? chalk.red('not visible') : chalk.cyan('visible')].join('')),
|
||||
([chalk.yellow('AutoIndex: '), argv.i ? chalk.red('not visible') : chalk.cyan('visible')].join('')),
|
||||
([chalk.yellow('Serve GZIP Files: '), argv.g || argv.gzip ? chalk.cyan('true') : chalk.red('false')].join('')),
|
||||
([chalk.yellow('Serve Brotli Files: '), argv.b || argv.brotli ? chalk.cyan('true') : chalk.red('false')].join('')),
|
||||
([chalk.yellow('Default File Extension: '), argv.e ? chalk.cyan(argv.e) : (argv.ext ? chalk.cyan(argv.ext) : chalk.red('none'))].join(''))
|
||||
].join('\n'));
|
||||
|
||||
logger.info(chalk.yellow('\nAvailable on:'));
|
||||
|
||||
if (argv.a && host !== '0.0.0.0') {
|
||||
logger.info(` ${protocol}${host}:${chalk.green(port.toString())}`);
|
||||
} else {
|
||||
Object.keys(ifaces).forEach(function (dev) {
|
||||
ifaces[dev].forEach(function (details) {
|
||||
if (details.family === 'IPv4') {
|
||||
logger.info((' ' + protocol + details.address + ':' + chalk.green(port.toString())));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof proxy === 'string') {
|
||||
if (proxyOptions) {
|
||||
logger.info('Unhandled requests will be served from: ' + proxy + '. Options: ' + JSON.stringify(proxyOptions));
|
||||
}
|
||||
else {
|
||||
logger.info('Unhandled requests will be served from: ' + proxy);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Hit CTRL-C to stop the server');
|
||||
if (argv.o) {
|
||||
const openHost = host === '0.0.0.0' ? '127.0.0.1' : host;
|
||||
let openUrl = `${protocol}${openHost}:${port}`;
|
||||
if (typeof argv.o === 'string') {
|
||||
openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o;
|
||||
}
|
||||
logger.info('Open: ' + openUrl);
|
||||
opener(openUrl);
|
||||
}
|
||||
|
||||
// Spacing before logs
|
||||
if (!argv.s) logger.info();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
}).on('SIGINT', function () {
|
||||
process.emit('SIGINT');
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
logger.info(chalk.red('http-server stopped.'));
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
logger.info(chalk.red('http-server stopped.'));
|
||||
process.exit();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"iterations": [
|
||||
{
|
||||
"iteration": 1,
|
||||
"startedAt": "2026-02-11T22:50:15.397Z",
|
||||
"endedAt": "2026-02-11T22:55:02.081Z",
|
||||
"durationMs": 283525,
|
||||
"toolsUsed": {},
|
||||
"filesModified": [
|
||||
"Ralph/IMPLEMENTATION_PLAN.md",
|
||||
"Ralph/progress.txt",
|
||||
"src/App.tsx",
|
||||
"src/components/BootSequence.tsx",
|
||||
"src/components/ECGAnimation.tsx"
|
||||
],
|
||||
"exitCode": 0,
|
||||
"completionDetected": false,
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"totalDurationMs": 283525,
|
||||
"struggleIndicators": {
|
||||
"repeatedErrors": {},
|
||||
"noProgressIterations": 0,
|
||||
"shortIterations": 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,72 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI agents (OpenCode, Claude Code, etc.) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Interactive CV/portfolio website for Andy Charlwood with a distinctive loading experience: terminal boot sequence → ECG canvas animation with name tracing. Built as a React SPA with TypeScript and Vite.
|
||||
|
||||
## Commands
|
||||
|
||||
- `npm run dev` — Start dev server (localhost:5173)
|
||||
- `npm run build` — TypeScript compile + Vite production build
|
||||
- `npm run typecheck` — TypeScript type checking only (`tsc --noEmit`)
|
||||
- `npm run lint` — ESLint
|
||||
- `npm run preview` — Preview production build
|
||||
|
||||
No test framework is configured.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Loading UI Flow
|
||||
|
||||
`App.tsx` manages a `Phase` state (`'boot'` → `'ecg'`). Each phase renders exclusively:
|
||||
|
||||
1. **BootSequence** — Terminal typing animation (~4s), green-on-black aesthetic
|
||||
2. **ECGAnimation** — Canvas-based heartbeat animation (~5-6s) with letter tracing, background transitions from black to white
|
||||
|
||||
Total boot-to-ECG completion time must be ≤10 seconds.
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Canvas ECG**: `ECGAnimation.tsx` does imperative canvas drawing with requestAnimationFrame — flatline → 3 heartbeats (40px→60px→100px) → letter tracing → exit.
|
||||
|
||||
### Path Aliases
|
||||
|
||||
`@/` maps to `./src/` (configured in both `vite.config.ts` and `tsconfig.json`).
|
||||
|
||||
### Styling
|
||||
|
||||
Tailwind CSS with custom design tokens in `tailwind.config.js`:
|
||||
- **Colors**: teal `#00897B` (primary), coral `#FF6B6B` (accent), ECG palette (green/cyan/dim)
|
||||
- **Fonts**: Plus Jakarta Sans (primary), Inter Tight (secondary), Fira Code (mono/terminal)
|
||||
- **Breakpoints**: xs 480px, sm 640px, md 768px, lg 1024px, xl 1280px
|
||||
|
||||
### Type System
|
||||
|
||||
All data types live in `src/types/index.ts`. Strict TypeScript — no `any` types. One component per file with typed props interfaces.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Boot sequence text and colors must match `References/concept.html` exactly (CLINICAL TERMINAL v3.2.1 format).
|
||||
- ECG animation timing/amplitudes/color transitions must match the concept reference.
|
||||
- When writing components with visual styling or animations, invoke the `frontend-design` skill first.
|
||||
|
||||
## Available Skills
|
||||
|
||||
This project has access to the following agent skills in `.agents/skills/`:
|
||||
- **frontend-design** — Use for any visual styling or animation work
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # One component per file (PascalCase)
|
||||
├── hooks/ # Custom hooks (camelCase, use* prefix)
|
||||
├── lib/ # Utility functions
|
||||
├── types/ # TypeScript interfaces
|
||||
├── App.tsx # Phase manager (root component)
|
||||
└── index.css # Global styles + Tailwind directives
|
||||
Ralph/ # Implementation plan, guardrails, progress tracking
|
||||
References/ # Source content (concept.html, ECGVideo/)
|
||||
```
|
||||
@@ -4,7 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Interactive CV/portfolio website for Andy Charlwood with a distinctive three-phase loading experience: terminal boot sequence → ECG canvas animation → main content. Built as a React SPA with TypeScript and Vite.
|
||||
Interactive CV/portfolio for Andy Charlwood, presented as a GP clinical record system. The concept: *what if a GP surgery's patient record system were redesigned by a luxury product studio?* The structure and metaphor of a real clinical system (tiles as record sections, status indicators, medication-style skill entries, alerts) — but elevated with refined typography, considered motion, and a modern light aesthetic.
|
||||
|
||||
**This is NOT a faithful NHS system clone.** It's a showcase portfolio that *evokes* the feel of clinical software while being distinctly beautiful. The clinical metaphor is the creative conceit; the execution should feel premium and contemporary.
|
||||
|
||||
Built as a React SPA with TypeScript and Vite.
|
||||
|
||||
**Reference design:** `References/GPSystemconcept.html` — the visual and structural target for the dashboard.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -18,57 +24,195 @@ No test framework is configured.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Three-Phase UI Flow
|
||||
### Four-Phase UI Flow
|
||||
|
||||
`App.tsx` manages a `Phase` state (`'boot'` → `'ecg'` → `'content'`). Each phase renders exclusively:
|
||||
`App.tsx` manages a `Phase` state (`'boot'` → `'ecg'` → `'login'` → `'dashboard'`). Each phase renders exclusively:
|
||||
|
||||
1. **BootSequence** — Terminal typing animation (~4s), green-on-black aesthetic
|
||||
2. **ECGAnimation** — Canvas-based heartbeat animation (~5-6s) with letter tracing, background transitions from black to white
|
||||
3. **Content** — FloatingNav + all CV sections (Hero, Skills, Experience, Education, Projects, Contact, Footer)
|
||||
1. **BootSequence** — Terminal typing animation (~4s), green-on-black aesthetic. Fira Code font, matrix-green palette. **Locked — do not change.**
|
||||
2. **ECGAnimation** — Canvas-based heartbeat animation with mask-based letter tracing. Background transitions from black to `#1E293B`. **Locked — do not change.**
|
||||
3. **LoginScreen** — Animated login card on dark background. Types credentials at a natural pace, then presents an interactive "Log In" button for the user to click. Login transitions to the dashboard.
|
||||
4. **DashboardLayout** — The main portfolio experience: TopBar + Sidebar + scrollable tile-based dashboard.
|
||||
|
||||
Total boot-to-content time must be ≤10 seconds.
|
||||
### Dashboard Layout (Post-Login)
|
||||
|
||||
The dashboard uses a three-zone layout:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ TopBar (fixed, 48px) — brand, search, session │
|
||||
├──────────┬──────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Sidebar │ Card Grid (scrollable) │
|
||||
│ (272px) │ ┌─────────────────────────────────┐ │
|
||||
│ │ │ Patient Summary (full width) │ │
|
||||
│ Person │ ├────────────────┬────────────────┤ │
|
||||
│ Header │ │ Latest Results │ Repeat Meds │ │
|
||||
│ │ │ (KPIs) │ (Core Skills) │ │
|
||||
│ Tags │ ├────────────────┴────────────────┤ │
|
||||
│ │ │ Last Consultation (full width) │ │
|
||||
│ Alerts │ ├─────────────────────────────────┤ │
|
||||
│ │ │ Career Activity (full width) │ │
|
||||
│ │ ├─────────────────────────────────┤ │
|
||||
│ │ │ Education (full width) │ │
|
||||
│ │ ├─────────────────────────────────┤ │
|
||||
│ │ │ Projects (full width) │ │
|
||||
│ │ └─────────────────────────────────┘ │
|
||||
└──────────┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**No view switching.** The dashboard is a single scrollable page of tiles. Users scroll to see all sections. Detail drill-down happens by expanding tiles in-place (accordion pattern).
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Scroll reveals**: `useScrollReveal` hook wraps IntersectionObserver with trigger-once semantics. Used by every content section. Never use scroll event listeners.
|
||||
- **Active nav tracking**: `useActiveSection` hook tracks which section is in viewport for FloatingNav highlighting.
|
||||
- **Staggered animations**: Components use index-based delays (`baseDelay + index * 100`) with Framer Motion.
|
||||
- **SVG skill circles**: `Skills.tsx` uses `strokeDashoffset = circumference * (1 - level / 100)` with `-90deg` rotation to start from 12 o'clock.
|
||||
- **Canvas ECG**: `ECGAnimation.tsx` does imperative canvas drawing with requestAnimationFrame — flatline → 3 heartbeats (40px→60px→100px) → letter tracing → exit.
|
||||
- **Canvas ECG**: `ECGAnimation.tsx` does imperative canvas drawing with requestAnimationFrame — flatline → 3 heartbeats (40px→60px→100px) → mask-based letter tracing → exit. **Locked — do not change.**
|
||||
- **TopBar**: `TopBar.tsx` — fixed at top, brand + search trigger + session info. Search bar triggers Command Palette on click/Ctrl+K.
|
||||
- **Sidebar**: `Sidebar.tsx` — light background, contains PersonHeader (avatar, name, title, status, details), Tags, and Alerts only. Skills, Projects, Education are in the main content tiles.
|
||||
- **Card Grid**: CSS Grid, 2 columns on desktop (gap 16px), 1 column on mobile. Tiles use a reusable `Card` component with consistent styling.
|
||||
- **Tile Expansion**: Career Activity items, Project items, and Skill items expand in-place with height-only animation (200ms, ease-out). Single-expand accordion — only one item open at a time.
|
||||
- **KPI Flip Cards**: Latest Results metrics flip on click to show explanation text. CSS perspective transform, 400ms.
|
||||
- **Command Palette**: Ctrl+K opens a Spotlight-style search overlay. Fuzzy search via fuse.js. Keyboard navigation (arrow keys, Enter, Escape).
|
||||
- **Staggered entrance**: TopBar slides down → Sidebar slides from left → Content fades in. Quick (200-300ms).
|
||||
- **Expandable content**: Height-only animation, 200ms ease-out. Content grows/shrinks — no opacity fade.
|
||||
- **Responsive breakpoints**: Desktop (full sidebar + 2-col grid), Tablet (collapsed/hidden sidebar + 1-col), Mobile (no sidebar, stacked tiles).
|
||||
|
||||
### Path Aliases
|
||||
|
||||
`@/` maps to `./src/` (configured in both `vite.config.ts` and `tsconfig.json`).
|
||||
|
||||
### Styling
|
||||
|
||||
Tailwind CSS with custom design tokens in `tailwind.config.js`:
|
||||
- **Colors**: teal `#00897B` (primary), coral `#FF6B6B` (accent), ECG palette (green/cyan/dim)
|
||||
- **Fonts**: Plus Jakarta Sans (primary), Inter Tight (secondary), Fira Code (mono/terminal)
|
||||
- **Breakpoints**: xs 480px, sm 640px, md 768px, lg 1024px, xl 1280px
|
||||
- Inline styles only for dynamic values that Tailwind can't express (e.g., computed `strokeDashoffset`).
|
||||
|
||||
### Type System
|
||||
|
||||
All data types live in `src/types/index.ts`. Strict TypeScript — no `any` types. One component per file with typed props interfaces.
|
||||
All data types live in `src/types/index.ts` and `src/types/pmr.ts`. Strict TypeScript — no `any` types. One component per file with typed props interfaces.
|
||||
|
||||
## Design Direction: GP System Dashboard
|
||||
|
||||
The aesthetic direction is a **modern GP system dashboard** — the precision and information density of a medical records system, but with a light, contemporary, premium feel. Think: a healthcare SaaS product redesigned by a Swiss product studio.
|
||||
|
||||
### Tone
|
||||
|
||||
- **Precise, not cold.** Every element has a reason. Spacing is generous but intentional.
|
||||
- **Light, not washed out.** Warm sage background, clean white surfaces, deliberate color accents.
|
||||
- **Technical, not sterile.** Monospace data, status indicators, and coded entries create authentic texture.
|
||||
- **Elegant, not decorative.** No gratuitous ornament. Beauty comes from proportion, contrast, and type.
|
||||
|
||||
### Typography
|
||||
|
||||
Typography is the primary vehicle for premium feel. Avoid generic system fonts.
|
||||
|
||||
- **UI / Body:**
|
||||
- **Elvaro Grotesque** (primary, `font-ui`) — Modern grotesque sans-serif. 7 weights (300-900). Institutional credibility with premium feel. Slightly condensed proportions suit data-dense UI.
|
||||
- **Blumir** (alternative, `font-ui-alt`) — Geometric-humanist hybrid. Variable font (100-700). More refined/luxurious feel.
|
||||
- Both fonts sourced from Envato (licensed), stored in `Fonts/`. **Do not use Inter, Roboto, DM Sans, or system defaults.**
|
||||
- Font files: Elvaro `Fonts/Elvaro Grotesque Sans Family/WOFF/TBJElvaro-*.woff2`, Blumir `Fonts/blumir-font-family/WOFF/Blumir-VF.woff2`
|
||||
- **Monospace / Data**: Geist Mono for timestamps, session info, GPhC number, dates, coded entries. Creates "technical texture."
|
||||
- **Terminal phase**: Fira Code — locked, do not change.
|
||||
- **Type scale**: Tight. Headings 15-18px, body 12.5-14px, labels 10-12px. Precision over drama.
|
||||
- **Weight hierarchy**: Use weight (400/500/600/700) rather than size to establish hierarchy.
|
||||
|
||||
### Color Palette
|
||||
|
||||
The palette anchors on teal as the primary accent, with a light sidebar + warm content background.
|
||||
|
||||
- **Teal `#0D6E6E`** — Primary accent. Active states, links, avatar gradient, interactive elements. Hover: `#0A8080`. Light: `rgba(10,128,128,0.08)`.
|
||||
- **Background `#F0F5F4`** — Warm sage. The content area feels organic, not flat gray.
|
||||
- **Sidebar `#F7FAFA`** — Very light. Right border `#D4E0DE` separates from content.
|
||||
- **TopBar `#FFFFFF`** — White surface. Bottom border `#D4E0DE`.
|
||||
- **Cards `#FFFFFF`** — White with shadow-sm and border-light. Hover deepens to shadow-md.
|
||||
- **Status colors**: Success `#059669`, Amber `#D97706`, Alert `#DC2626`, Purple `#7C3AED` — each with light bg and border variants. Always paired with text labels.
|
||||
- **Text**: Primary `#1A2B2A`, Secondary `#5B7A78`, Tertiary `#8DA8A5`. Use full range for hierarchy.
|
||||
- **Borders**: Structural `#D4E0DE`, Cards/inner `#E4EDEB`.
|
||||
|
||||
### Shadows & Depth
|
||||
|
||||
Three-tier shadow system for layered depth:
|
||||
|
||||
- **Cards (resting)**: `0 1px 2px rgba(26,43,42,0.05)` — gentle, always present.
|
||||
- **Cards (hover/interactive)**: `0 2px 8px rgba(26,43,42,0.08)` — slightly lifted.
|
||||
- **Overlays (command palette, modals)**: `0 8px 32px rgba(26,43,42,0.12)` — clearly elevated.
|
||||
- **Hover states**: Shadow deepens + border color strengthens. Subtle, not dramatic.
|
||||
|
||||
### Motion
|
||||
|
||||
Motion should feel considered and premium, never flashy:
|
||||
|
||||
- **Entrance animations**: Dashboard materializes in sequence — TopBar slides down → Sidebar slides from left → Content fades in. Quick (200-300ms) with easing.
|
||||
- **Login typing**: 80ms/char for username, 60ms/dot for password. Natural, readable pace. After typing completes, "Log In" button becomes interactive — user clicks to proceed.
|
||||
- **Login transition**: On button click, card scales slightly and fades. Transition to dashboard layout.
|
||||
- **Tile expansion**: Height-only animation, 200ms ease-out. Content grows/shrinks — no opacity fade.
|
||||
- **KPI flip**: CSS perspective rotateY, 400ms ease-in-out. Click to flip, click to flip back.
|
||||
- **Command palette**: Scale 0.97→1.0 + translateY entrance, 200ms. Backdrop fade.
|
||||
- **Hover states**: Subtle, immediate. Border color shifts, shadow deepens. Think: OS-level responsiveness.
|
||||
- **`prefers-reduced-motion`**: All animations skip to final state. No exceptions.
|
||||
|
||||
### Spatial Composition
|
||||
|
||||
- **Generous but structured.** Cards have 20px padding. Tile grid has 16px gap. Sections breathe.
|
||||
- **Clear visual hierarchy.** Card headers: uppercase, small (12px), tracked-out, secondary color with colored dot indicator.
|
||||
- **Two-column grid** on desktop, single column on mobile. Full-width tiles span both columns.
|
||||
- **Sidebar sections** separated by thin divider titles (10px, uppercase, tertiary, with line extending right).
|
||||
|
||||
### What Makes It Memorable
|
||||
|
||||
The distinctiveness comes from the *clinical metaphor applied to a modern interface*:
|
||||
- A light, professional sidebar with clinical-style person header and alert flags
|
||||
- Skills presented as "Repeat Medications" with frequency dosing (twice daily, when required)
|
||||
- KPI metrics that flip to reveal explanations, like interactive test results
|
||||
- Career history as a clinical timeline with color-coded entry types
|
||||
- The boot sequence → ECG → login flow is theatrical in a way that real clinical software never is
|
||||
- Command palette (Ctrl+K) for searching records, like a clinical search tool
|
||||
|
||||
## Styling
|
||||
|
||||
Tailwind CSS with custom design tokens in `tailwind.config.js`:
|
||||
- **Color tokens**: PMR-prefixed tokens (`pmr-accent`, `pmr-bg`, `pmr-surface`, `pmr-sidebar`, `pmr-text-primary`, etc.)
|
||||
- **Fonts**: `font-ui` (Elvaro Grotesque), `font-ui-alt` (Blumir), `font-geist` (Geist Mono), `font-mono` (Fira Code for terminal)
|
||||
- **Breakpoints**: xs 480px, sm 640px, md 768px, lg 1024px, xl 1280px
|
||||
- **Border radius**: 8px default for cards/tiles (`var(--radius)`). 6px for inner elements (`var(--radius-sm)`). 12px exception for login card and command palette.
|
||||
- **Shadows**: `shadow-sm`, `shadow-md`, `shadow-lg` tokens matching three-tier system.
|
||||
- CSS custom properties in `index.css` for both boot/ECG phase tokens and dashboard phase tokens.
|
||||
- Inline styles only for dynamic values that Tailwind can't express.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Boot sequence text and colors must match `References/concept.html` exactly (CLINICAL TERMINAL v3.2.1 format).
|
||||
- ECG animation timing/amplitudes/color transitions must match the concept reference.
|
||||
- CV content sourced from `References/CV_v4.md` — roles, dates, and achievement numbers must be accurate.
|
||||
- Icons via `lucide-react`, not unicode symbols.
|
||||
- **Boot sequence**: Text, colors, and timing must match `References/concept.html` exactly. **Do not modify.**
|
||||
- **ECG animation**: Timing, amplitudes, color transitions, and mask-based text reveal must match the concept reference. **Do not modify.**
|
||||
- **Reference design**: `References/GPSystemconcept.html` is the visual and structural target for the dashboard.
|
||||
- **CV content**: Sourced from `References/CV_v4.md` — roles, dates, and achievement numbers must be accurate.
|
||||
- **Icons**: Via `lucide-react`, not unicode symbols.
|
||||
- **Accessibility**: WCAG 2.1 AA compliance. Semantic HTML, ARIA attributes, keyboard navigation, `prefers-reduced-motion` support throughout. Status indicators always paired with text labels.
|
||||
- **No generic aesthetics**: Every design decision should feel intentional. If a component could appear in any random SaaS template, it needs more character.
|
||||
- **Fonts**: Elvaro Grotesque (primary) or Blumir (alt). Never Inter, Roboto, DM Sans, or system defaults. DM Sans appears in the concept HTML as a placeholder only.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # One component per file (PascalCase)
|
||||
├── hooks/ # Custom hooks (camelCase, use* prefix)
|
||||
├── lib/ # Utility functions
|
||||
├── types/ # TypeScript interfaces
|
||||
├── App.tsx # Phase manager (root component)
|
||||
└── index.css # Global styles + Tailwind directives
|
||||
Ralph/ # Implementation plan, guardrails, progress tracking
|
||||
References/ # Source content (concept.html, CV_v4.md, ECGVideo/)
|
||||
├── components/ # One component per file (PascalCase)
|
||||
│ ├── tiles/ # Dashboard tile components (PatientSummaryTile, LatestResultsTile, etc.)
|
||||
│ ├── views/ # Legacy PMR views (being replaced by tiles — may be referenced during transition)
|
||||
│ ├── TopBar.tsx # Fixed top bar (brand, search trigger, session)
|
||||
│ ├── Sidebar.tsx # Light sidebar (person header, tags, alerts)
|
||||
│ ├── DashboardLayout.tsx # Main layout (topbar + sidebar + card grid)
|
||||
│ ├── Card.tsx # Reusable card component with header
|
||||
│ ├── CommandPalette.tsx # Ctrl+K search overlay
|
||||
│ └── ... # Boot, ECG, Login (unchanged)
|
||||
├── contexts/ # React contexts (AccessibilityContext)
|
||||
├── data/ # Static data files
|
||||
│ ├── patient.ts # Person details
|
||||
│ ├── consultations.ts # Career roles (used in Last Consultation + Career Activity)
|
||||
│ ├── medications.ts # Legacy skill data
|
||||
│ ├── problems.ts # Achievements
|
||||
│ ├── investigations.ts # Projects
|
||||
│ ├── documents.ts # Education entries
|
||||
│ ├── profile.ts # Personal statement
|
||||
│ ├── tags.ts # Sidebar tags
|
||||
│ ├── alerts.ts # Sidebar alert flags
|
||||
│ ├── kpis.ts # KPI metrics for Latest Results
|
||||
│ └── skills.ts # Skills with frequency/years (medication metaphor)
|
||||
├── hooks/ # Custom hooks (camelCase, use* prefix)
|
||||
├── lib/ # Utility functions (search.ts for fuse.js)
|
||||
├── types/ # TypeScript interfaces (index.ts, pmr.ts)
|
||||
├── App.tsx # Phase manager (root component)
|
||||
└── index.css # Global styles + Tailwind directives
|
||||
Ralph/ # Implementation plan, guardrails, progress tracking
|
||||
References/ # Source content (concept.html, GPSystemconcept.html, CV_v4.md)
|
||||
```
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user