Redesign CVMIS system 2
This commit is contained in:
@@ -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();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
# Reference: Task 1 — Design Tokens and Tailwind Config
|
||||
|
||||
## Overview
|
||||
|
||||
Update the design system from the dark-sidebar NHS Blue palette to the GP System concept's light teal palette. The concept reference is `References/GPSystemconcept.html`.
|
||||
|
||||
## CSS Custom Properties (`src/index.css`)
|
||||
|
||||
Add/update these variables in the PMR section (keep boot/ECG/login variables unchanged):
|
||||
|
||||
```css
|
||||
/* GP System Dashboard tokens */
|
||||
--bg: #F0F5F4;
|
||||
--surface: #FFFFFF;
|
||||
--sidebar-bg: #F7FAFA;
|
||||
--text-primary: #1A2B2A;
|
||||
--text-secondary: #5B7A78;
|
||||
--text-tertiary: #8DA8A5;
|
||||
--accent: #0D6E6E;
|
||||
--accent-hover: #0A8080;
|
||||
--accent-light: rgba(10,128,128,0.08);
|
||||
--accent-border: rgba(10,128,128,0.18);
|
||||
--amber: #D97706;
|
||||
--amber-light: rgba(217,119,6,0.08);
|
||||
--amber-border: rgba(217,119,6,0.18);
|
||||
--success: #059669;
|
||||
--success-light: rgba(5,150,105,0.08);
|
||||
--success-border: rgba(5,150,105,0.18);
|
||||
--alert: #DC2626;
|
||||
--alert-light: rgba(220,38,38,0.08);
|
||||
--alert-border: rgba(220,38,38,0.18);
|
||||
--border: #D4E0DE;
|
||||
--border-light: #E4EDEB;
|
||||
--sidebar-width: 272px;
|
||||
--topbar-height: 48px;
|
||||
--radius: 8px;
|
||||
--radius-sm: 6px;
|
||||
--shadow-sm: 0 1px 2px rgba(26,43,42,0.05);
|
||||
--shadow-md: 0 2px 8px rgba(26,43,42,0.08);
|
||||
--shadow-lg: 0 8px 32px rgba(26,43,42,0.12);
|
||||
--font-body: var(--font-ui);
|
||||
--font-mono: 'Geist Mono', 'Fira Code', monospace;
|
||||
```
|
||||
|
||||
## Tailwind Config (`tailwind.config.js`)
|
||||
|
||||
Update the `extend` section:
|
||||
|
||||
### Colors
|
||||
```js
|
||||
colors: {
|
||||
'pmr-bg': '#F0F5F4',
|
||||
'pmr-surface': '#FFFFFF',
|
||||
'pmr-sidebar': '#F7FAFA',
|
||||
'pmr-accent': '#0D6E6E',
|
||||
'pmr-accent-hover': '#0A8080',
|
||||
'pmr-text-primary': '#1A2B2A',
|
||||
'pmr-text-secondary': '#5B7A78',
|
||||
'pmr-text-tertiary': '#8DA8A5',
|
||||
'pmr-border': '#D4E0DE',
|
||||
'pmr-border-light': '#E4EDEB',
|
||||
'pmr-success': '#059669',
|
||||
'pmr-amber': '#D97706',
|
||||
'pmr-alert': '#DC2626',
|
||||
'pmr-purple': '#7C3AED',
|
||||
// Keep pmr-nhsblue for backward compat during transition
|
||||
'pmr-nhsblue': '#005EB8',
|
||||
// Keep pmr-content as fallback
|
||||
'pmr-content': '#F0F5F4',
|
||||
}
|
||||
```
|
||||
|
||||
### Shadows
|
||||
```js
|
||||
boxShadow: {
|
||||
'pmr-sm': '0 1px 2px rgba(26,43,42,0.05)',
|
||||
'pmr-md': '0 2px 8px rgba(26,43,42,0.08)',
|
||||
'pmr-lg': '0 8px 32px rgba(26,43,42,0.12)',
|
||||
// Keep old pmr shadow as alias during transition
|
||||
'pmr': '0 1px 2px rgba(26,43,42,0.05)',
|
||||
}
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
```js
|
||||
borderRadius: {
|
||||
'card': '8px', // was 4px — now 8px per concept
|
||||
'card-sm': '6px', // inner elements
|
||||
'login': '12px', // login card exception
|
||||
}
|
||||
```
|
||||
|
||||
## What NOT to Change
|
||||
|
||||
- Boot phase variables (`--matrix-*`, `--terminal-*`)
|
||||
- ECG phase variables
|
||||
- Login phase background (`#1E293B` — handled by transition)
|
||||
- Font declarations (Elvaro, Blumir, Geist Mono, Fira Code already set up correctly)
|
||||
- Breakpoint values
|
||||
@@ -0,0 +1,203 @@
|
||||
# Reference: Task 2 — Data Files and Types
|
||||
|
||||
## Overview
|
||||
|
||||
Create new data files for dashboard-specific content and update the type system. All CV content must match `References/CV_v4.md` exactly.
|
||||
|
||||
## New Data Files
|
||||
|
||||
### `src/data/profile.ts`
|
||||
|
||||
```typescript
|
||||
export const personalStatement = `Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insights—from financial scenario modelling and pharmaceutical rebate negotiation to algorithm design and population-level pathway development. Proven track record of identifying and prioritising efficiency programmes worth £14.6M+ through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for executive stakeholders.`
|
||||
```
|
||||
|
||||
### `src/data/tags.ts`
|
||||
|
||||
```typescript
|
||||
import type { Tag } from '@/types/pmr'
|
||||
|
||||
export const tags: Tag[] = [
|
||||
{ label: 'Pharmacist', colorVariant: 'teal' },
|
||||
{ label: 'Data Lead', colorVariant: 'teal' },
|
||||
{ label: 'NHS', colorVariant: 'teal' },
|
||||
{ label: 'Population Health', colorVariant: 'amber' },
|
||||
{ label: 'BI & Analytics', colorVariant: 'green' },
|
||||
]
|
||||
```
|
||||
|
||||
### `src/data/alerts.ts`
|
||||
|
||||
```typescript
|
||||
import type { Alert } from '@/types/pmr'
|
||||
|
||||
export const alerts: Alert[] = [
|
||||
{
|
||||
message: '£14.6M SAVINGS IDENTIFIED',
|
||||
severity: 'alert',
|
||||
icon: 'AlertTriangle', // lucide-react icon name
|
||||
},
|
||||
{
|
||||
message: '£220M BUDGET OVERSIGHT',
|
||||
severity: 'amber',
|
||||
icon: 'AlertCircle', // lucide-react icon name
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### `src/data/kpis.ts`
|
||||
|
||||
```typescript
|
||||
import type { KPI } from '@/types/pmr'
|
||||
|
||||
export const kpis: KPI[] = [
|
||||
{
|
||||
id: 'budget',
|
||||
value: '£220M',
|
||||
label: 'Budget Oversight',
|
||||
sub: 'NHS prescribing',
|
||||
colorVariant: 'green',
|
||||
explanation: 'Managed the ICB\'s total prescribing budget with sophisticated forecasting models identifying cost pressures and enabling proactive financial planning across Norfolk & Waveney.',
|
||||
},
|
||||
{
|
||||
id: 'savings',
|
||||
value: '£14.6M',
|
||||
label: 'Efficiency Savings',
|
||||
sub: 'Identified & tracked',
|
||||
colorVariant: 'amber',
|
||||
explanation: 'Identified and prioritised a £14.6M efficiency programme through comprehensive data analysis; achieved over-target performance through targeted, evidence-based interventions across the integrated care system.',
|
||||
},
|
||||
{
|
||||
id: 'years',
|
||||
value: '9+',
|
||||
label: 'Years in NHS',
|
||||
sub: 'Since 2016',
|
||||
colorVariant: 'teal',
|
||||
explanation: 'Continuous NHS service since August 2016, progressing from community pharmacy through prescribing data analysis to system-level population health data leadership.',
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
value: '12',
|
||||
label: 'Team Size Led',
|
||||
sub: 'Cross-functional',
|
||||
colorVariant: 'green',
|
||||
explanation: 'Led a cross-functional team of 12 spanning data analysts, population health specialists, and pharmacists across data, analytics, and population health workstreams.',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### `src/data/skills.ts`
|
||||
|
||||
Skills presented as "medications" with frequency (user-specified values) and years of experience.
|
||||
|
||||
```typescript
|
||||
import type { SkillMedication } from '@/types/pmr'
|
||||
|
||||
export const skills: SkillMedication[] = [
|
||||
{
|
||||
id: 'data-analysis',
|
||||
name: 'Data Analysis',
|
||||
frequency: 'Twice daily',
|
||||
startYear: 2016,
|
||||
yearsOfExperience: 9,
|
||||
proficiency: 95,
|
||||
category: 'Technical',
|
||||
status: 'Active',
|
||||
icon: 'BarChart3',
|
||||
},
|
||||
{
|
||||
id: 'python',
|
||||
name: 'Python',
|
||||
frequency: 'Daily',
|
||||
startYear: 2019,
|
||||
yearsOfExperience: 6,
|
||||
proficiency: 90,
|
||||
category: 'Technical',
|
||||
status: 'Active',
|
||||
icon: 'Code2',
|
||||
},
|
||||
{
|
||||
id: 'sql',
|
||||
name: 'SQL',
|
||||
frequency: 'Daily',
|
||||
startYear: 2018,
|
||||
yearsOfExperience: 7,
|
||||
proficiency: 88,
|
||||
category: 'Technical',
|
||||
status: 'Active',
|
||||
icon: 'Database',
|
||||
},
|
||||
{
|
||||
id: 'power-bi',
|
||||
name: 'Power BI',
|
||||
frequency: 'Once weekly',
|
||||
startYear: 2020,
|
||||
yearsOfExperience: 5,
|
||||
proficiency: 92,
|
||||
category: 'Technical',
|
||||
status: 'Active',
|
||||
icon: 'PieChart',
|
||||
},
|
||||
{
|
||||
id: 'javascript-typescript',
|
||||
name: 'JavaScript / TypeScript',
|
||||
frequency: 'When required',
|
||||
startYear: 2022,
|
||||
yearsOfExperience: 3,
|
||||
proficiency: 70,
|
||||
category: 'Technical',
|
||||
status: 'Active',
|
||||
icon: 'FileCode2',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Note: Additional domain/leadership skills can be added later. Start with the 5 technical skills the user specified frequencies for.
|
||||
|
||||
## Type Updates (`src/types/pmr.ts`)
|
||||
|
||||
Add these interfaces (keep all existing types):
|
||||
|
||||
```typescript
|
||||
export interface Tag {
|
||||
label: string
|
||||
colorVariant: 'teal' | 'amber' | 'green'
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
message: string
|
||||
severity: 'alert' | 'amber'
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface KPI {
|
||||
id: string
|
||||
value: string
|
||||
label: string
|
||||
sub: string
|
||||
colorVariant: 'green' | 'amber' | 'teal'
|
||||
explanation: string
|
||||
}
|
||||
|
||||
export interface SkillMedication {
|
||||
id: string
|
||||
name: string
|
||||
frequency: string
|
||||
startYear: number
|
||||
yearsOfExperience: number
|
||||
proficiency: number
|
||||
category: 'Technical' | 'Domain' | 'Leadership'
|
||||
status: 'Active' | 'Historical'
|
||||
icon: string
|
||||
}
|
||||
```
|
||||
|
||||
## Existing Data — No Changes
|
||||
|
||||
These files remain untouched:
|
||||
- `src/data/patient.ts`
|
||||
- `src/data/consultations.ts`
|
||||
- `src/data/medications.ts`
|
||||
- `src/data/problems.ts`
|
||||
- `src/data/investigations.ts`
|
||||
- `src/data/documents.ts`
|
||||
@@ -0,0 +1,147 @@
|
||||
# Reference: Tasks 4-6 — TopBar and Sidebar
|
||||
|
||||
## Concept Reference
|
||||
|
||||
All specs below are derived from `References/GPSystemconcept.html`. Open it in a browser for visual reference.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: TopBar Component
|
||||
|
||||
### File: `src/components/TopBar.tsx`
|
||||
|
||||
### Structure
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [🏠] Headhunt Medical Center Remote │ [🔍 Search... Ctrl+K] │ Dr. A.CHARLWOOD · Active Session · 12:23 [Ctrl+K] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Specs
|
||||
|
||||
**Container:**
|
||||
- `position: fixed`, `top: 0`, `left: 0`, `right: 0`
|
||||
- `height: var(--topbar-height)` (48px)
|
||||
- `background: var(--surface)` (#FFFFFF)
|
||||
- `border-bottom: 1px solid var(--border)` (#D4E0DE)
|
||||
- `display: flex`, `align-items: center`, `justify-content: space-between`
|
||||
- `padding: 0 20px`
|
||||
- `z-index: 100`
|
||||
|
||||
**Brand (left):**
|
||||
- `Home` icon from lucide-react (18px, accent color)
|
||||
- Text: "Headhunt Medical Center" — 13px, font-ui, 600 weight, text-primary
|
||||
- Version badge: "Remote" — 11px, 400 weight, text-tertiary, margin-left 2px
|
||||
|
||||
**Search bar (center):**
|
||||
- Wrapper: `max-width: 560px`, `min-width: 400px`
|
||||
- Container: `height: 42px`, `border: 1.5px solid var(--border)`, `border-radius: var(--radius)` (8px), `padding: 0 14px`, white bg
|
||||
- Search icon (16px, tertiary) + input + "Ctrl+K" kbd badge
|
||||
- Input: 13px, font-body, placeholder "Search records, experience, skills... (Ctrl+K)"
|
||||
- Hover: `border-color: var(--accent-border)`
|
||||
- Focus: `border-color: var(--accent)`, `box-shadow: 0 0 0 3px rgba(13,110,110,0.12)`
|
||||
- **On click/focus: opens Command Palette** (Task 18). Does NOT do inline search.
|
||||
- Kbd badge: mono font, 10px, tertiary, bg: var(--bg), border, padding 2px 6px, radius 4px
|
||||
|
||||
**Session info (right):**
|
||||
- Text: "Dr. A.CHARLWOOD · Active Session · [time]" — 12px, text-secondary
|
||||
- Session pill: mono 11px, tertiary, `background: var(--accent-light)`, `padding: 3px 10px`, radius 4px, `border: 1px solid var(--accent-border)`
|
||||
- Ctrl+K shortcut badge (same style as search bar badge)
|
||||
|
||||
**Responsive:**
|
||||
- Mobile (<768px): hide center search bar. Show only brand + session info (or hamburger).
|
||||
- Tablet: search bar may shrink.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Sidebar — PersonHeader
|
||||
|
||||
### File: `src/components/Sidebar.tsx`
|
||||
|
||||
### Overall Sidebar Container
|
||||
- `width: var(--sidebar-width)` (272px)
|
||||
- `min-width: var(--sidebar-width)`
|
||||
- `background: var(--sidebar-bg)` (#F7FAFA)
|
||||
- `border-right: 1px solid var(--border)` (#D4E0DE)
|
||||
- `overflow-y: auto`, custom scrollbar (4px width, transparent track, border-colored thumb)
|
||||
- `padding: 20px 16px`
|
||||
- `display: flex`, `flex-direction: column`, `gap: 2px`
|
||||
|
||||
### PersonHeader Section
|
||||
Bordered below: `border-bottom: 2px solid var(--accent)`, `padding-bottom: 16px`, `margin-bottom: 6px`
|
||||
|
||||
**Avatar:**
|
||||
- 52px × 52px circle
|
||||
- `background: linear-gradient(135deg, var(--accent), #0A8080)`
|
||||
- White text "AC", 700 weight, 18px, centered
|
||||
- `box-shadow: 0 2px 8px rgba(13,110,110,0.25)`
|
||||
- `margin-bottom: 12px`
|
||||
|
||||
**Name:**
|
||||
- "CHARLWOOD, Andrew"
|
||||
- 15px, 700 weight, text-primary, `letter-spacing: -0.01em`
|
||||
|
||||
**Title:**
|
||||
- "Pharmacy Data Technologist"
|
||||
- 11.5px, mono font, 400 weight, text-secondary
|
||||
- `margin-top: 2px`
|
||||
|
||||
**Status badge:**
|
||||
- Inline-flex, gap 5px
|
||||
- `margin-top: 8px`
|
||||
- 11px, 500 weight, success color (#059669)
|
||||
- `background: var(--success-light)`, `border: 1px solid var(--success-border)`
|
||||
- `padding: 3px 9px`, `border-radius: 20px` (pill)
|
||||
- Animated dot: 6px circle, success color, `animation: pulse 2s infinite` (opacity 1→0.4→1)
|
||||
- Text: "Open to Opportunities"
|
||||
|
||||
**Details grid:**
|
||||
- `display: grid`, `grid-template-columns: 1fr`, `gap: 6px`, `margin-top: 12px`
|
||||
- Each row: `display: flex`, `justify-content: space-between`, `align-items: center`, 11.5px, `padding: 2px 0`
|
||||
- Label: text-tertiary, 400 weight
|
||||
- Value: text-primary, 500 weight, text-align right
|
||||
- GPhC No. value: mono font, 11px, `letter-spacing: 0.12em` → "2211810"
|
||||
- Education value: "MPharm 2.1 (Hons)"
|
||||
- Location: "Norwich, Norfolk"
|
||||
- Phone: link in accent color, `text-decoration: none`, underline on hover → "07795 553 088"
|
||||
- Email: link → "andy@charlwood.xyz"
|
||||
- Registered: "August 2016"
|
||||
|
||||
**Data source:** `src/data/patient.ts`
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Sidebar — Tags + Alerts
|
||||
|
||||
### Section Title Component
|
||||
Reusable within sidebar. Used for "Tags", "Alerts / Highlights", and any future sections.
|
||||
|
||||
- `font-size: 10px`, `font-weight: 600`, `text-transform: uppercase`, `letter-spacing: 0.08em`
|
||||
- Color: text-tertiary
|
||||
- `margin-bottom: 10px`
|
||||
- Flex row with `::after` pseudo-element: `flex: 1`, `height: 1px`, `background: var(--border-light)`, `gap: 6px`
|
||||
|
||||
### Tags Section
|
||||
- Container: `display: flex`, `flex-wrap: wrap`, `gap: 5px`
|
||||
- Each tag: 10.5px, 500 weight, `padding: 3px 8px`, `border-radius: 4px`, inline-flex, `line-height: 1.3`
|
||||
- **Color variants:**
|
||||
- `teal`: `background: var(--accent-light)`, `color: var(--accent)`, `border: 1px solid var(--accent-border)`
|
||||
- `amber`: `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
|
||||
- `green`: `background: var(--success-light)`, `color: var(--success)`, `border: 1px solid var(--success-border)`
|
||||
- **Data source:** `src/data/tags.ts`
|
||||
|
||||
### Alerts / Highlights Section
|
||||
- Container: `display: flex`, `flex-direction: column`, `gap: 6px`
|
||||
- Each flag item: `display: flex`, `align-items: center`, `gap: 8px`
|
||||
- 11px, 700 weight, `padding: 7px 10px`, `border-radius: var(--radius-sm)` (6px), `letter-spacing: 0.02em`
|
||||
- **Alert variant** (red):
|
||||
- `background: var(--alert-light)`, `color: var(--alert)`, `border: 1px solid var(--alert-border)`
|
||||
- Icon: `AlertTriangle` from lucide-react (14px, 2.5 stroke-width)
|
||||
- **Amber variant:**
|
||||
- `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
|
||||
- Icon: `AlertCircle` from lucide-react (14px, 2.5 stroke-width)
|
||||
- Icon container: 16px square, flex center, flex-shrink-0
|
||||
- **Data source:** `src/data/alerts.ts`
|
||||
|
||||
### Section Padding
|
||||
Each sidebar section: `padding: 14px 0 6px`
|
||||
@@ -0,0 +1,136 @@
|
||||
# Reference: Task 7 — DashboardLayout
|
||||
|
||||
## Overview
|
||||
|
||||
Create the main layout component that replaces `PMRInterface.tsx`. This is the container that houses TopBar, Sidebar, and the scrollable card grid of tiles.
|
||||
|
||||
## File: `src/components/DashboardLayout.tsx`
|
||||
|
||||
### Layout Structure
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ TopBar (fixed, z-100, height: 48px) │
|
||||
├──────────┬─────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Sidebar │ <main> — scrollable card grid │
|
||||
│ (272px) │ padding: 24px 28px 40px │
|
||||
│ fixed │ │
|
||||
│ │ grid: 1fr 1fr, gap: 16px │
|
||||
│ │ │
|
||||
│ │ [PatientSummary — full] │
|
||||
│ │ [LatestResults] [CoreSkills] │
|
||||
│ │ [LastConsultation — full] │
|
||||
│ │ [CareerActivity — full] │
|
||||
│ │ [Education — full] │
|
||||
│ │ [Projects — full] │
|
||||
│ │ │
|
||||
└──────────┴─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### CSS Layout
|
||||
|
||||
```
|
||||
.layout {
|
||||
display: flex;
|
||||
margin-top: var(--topbar-height); /* 48px */
|
||||
height: calc(100vh - var(--topbar-height));
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
/* See ref-03-topbar-sidebar.md for sidebar specs */
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
/* ... */
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 28px 40px;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use Tailwind classes for all of this — the CSS above is for reference only.
|
||||
|
||||
### Framer Motion Entrance Animations
|
||||
|
||||
Staggered entrance when dashboard first renders (after login):
|
||||
|
||||
1. **TopBar**: slides down from `-48px`, 200ms ease-out
|
||||
2. **Sidebar**: slides from `-272px` left, 250ms ease-out, 50ms delay
|
||||
3. **Main content**: fades in (opacity 0→1), 300ms, 150ms delay
|
||||
|
||||
```typescript
|
||||
const topbarVariants = {
|
||||
hidden: { y: -48, opacity: 0 },
|
||||
visible: { y: 0, opacity: 1, transition: { duration: 0.2, ease: 'easeOut' } }
|
||||
}
|
||||
|
||||
const sidebarVariants = {
|
||||
hidden: { x: -272, opacity: 0 },
|
||||
visible: { x: 0, opacity: 1, transition: { duration: 0.25, ease: 'easeOut', delay: 0.05 } }
|
||||
}
|
||||
|
||||
const contentVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { duration: 0.3, delay: 0.15 } }
|
||||
}
|
||||
```
|
||||
|
||||
With `prefers-reduced-motion`: all durations → 0, no delays.
|
||||
|
||||
### Tile Ordering in Grid
|
||||
|
||||
The card grid renders tiles in this order:
|
||||
1. `PatientSummaryTile` — `grid-column: 1 / -1` (full width)
|
||||
2. `LatestResultsTile` — single column (left)
|
||||
3. `CoreSkillsTile` — single column (right)
|
||||
4. `LastConsultationTile` — `grid-column: 1 / -1` (full width)
|
||||
5. `CareerActivityTile` — `grid-column: 1 / -1` (full width)
|
||||
6. `EducationTile` — `grid-column: 1 / -1` (full width)
|
||||
7. `ProjectsTile` — `grid-column: 1 / -1` (full width)
|
||||
|
||||
### App.tsx Wiring
|
||||
|
||||
In `src/App.tsx`, the PMR phase currently renders `<PMRInterface />`. Change it to render `<DashboardLayout />`.
|
||||
|
||||
```typescript
|
||||
// In App.tsx phase switch:
|
||||
case 'pmr':
|
||||
return <DashboardLayout />
|
||||
```
|
||||
|
||||
Keep all other phases (boot, ecg, login) unchanged. The SkipButton that skips to login should still work.
|
||||
|
||||
### Scrollbar Styling
|
||||
|
||||
Main content area scrollbar (matches concept):
|
||||
- Width: 6px
|
||||
- Track: transparent
|
||||
- Thumb: var(--border) (#D4E0DE), border-radius 3px
|
||||
|
||||
### Command Palette Integration
|
||||
|
||||
The DashboardLayout should render the `CommandPalette` component (from Task 18) at the layout level, so it overlays the entire dashboard when triggered. For now (Task 7), just add a placeholder comment or empty div where it will go. The TopBar search bar's click handler should be wired to open the palette (but the palette itself comes in Task 18).
|
||||
|
||||
### Background Color Transition
|
||||
|
||||
The login screen has background `#1E293B`. The dashboard has background `#F0F5F4`. This transition should happen smoothly. Options:
|
||||
1. The DashboardLayout entrance animation covers the transition (content fades in over the dark background, replacing it)
|
||||
2. A brief CSS transition on the body/root background color
|
||||
3. Handle it in App.tsx with a state-based background
|
||||
|
||||
The simplest approach is option 1 — the dashboard's entrance animation effectively replaces the dark login background with the light dashboard.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Reference: Tasks 8-11 — Card Component and Top Tiles
|
||||
|
||||
## Task 8: Reusable Card Component
|
||||
|
||||
### File: `src/components/Card.tsx`
|
||||
|
||||
### Base Card
|
||||
```typescript
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
full?: boolean // spans both grid columns
|
||||
className?: string
|
||||
}
|
||||
```
|
||||
|
||||
**Styling:**
|
||||
- `background: var(--surface)` (#FFFFFF)
|
||||
- `border: 1px solid var(--border-light)` (#E4EDEB)
|
||||
- `border-radius: var(--radius)` (8px)
|
||||
- `padding: 20px`
|
||||
- `box-shadow: var(--shadow-sm)` (0 1px 2px rgba(26,43,42,0.05))
|
||||
- Hover: `box-shadow: var(--shadow-md)`, `border-color: var(--border)` (#D4E0DE)
|
||||
- `transition: box-shadow 0.2s, border-color 0.2s`
|
||||
- Full variant: `grid-column: 1 / -1`
|
||||
|
||||
### CardHeader Sub-component
|
||||
```typescript
|
||||
interface CardHeaderProps {
|
||||
dotColor: 'teal' | 'amber' | 'green' | 'alert' | 'purple'
|
||||
title: string
|
||||
rightText?: string
|
||||
}
|
||||
```
|
||||
|
||||
**Styling:**
|
||||
- `display: flex`, `align-items: center`, `gap: 8px`, `margin-bottom: 16px`
|
||||
- Dot: 8px circle, `border-radius: 50%`, flex-shrink-0
|
||||
- teal: `#0D6E6E`, amber: `#D97706`, green: `#059669`, alert: `#DC2626`, purple: `#7C3AED`
|
||||
- Title: 12px, 600 weight, uppercase, `letter-spacing: 0.06em`, text-secondary (#5B7A78)
|
||||
- Right text (optional): 10px, 400 weight, normal case, no tracking, text-tertiary, mono font, `margin-left: auto`
|
||||
|
||||
---
|
||||
|
||||
## Task 9: PatientSummary Tile
|
||||
|
||||
### File: `src/components/tiles/PatientSummaryTile.tsx`
|
||||
|
||||
**Layout:** Full-width card, first in grid.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: teal dot + "PATIENT SUMMARY"
|
||||
- Body: personal statement text from `src/data/profile.ts`
|
||||
- Typography: 13px, font-ui, `line-height: 1.6` (leading-relaxed), text-primary
|
||||
- No interactive elements — read-only
|
||||
|
||||
**Data:** `import { personalStatement } from '@/data/profile'`
|
||||
|
||||
This is a simple tile. No expansion, no interactivity.
|
||||
|
||||
---
|
||||
|
||||
## Task 10: LatestResults Tile
|
||||
|
||||
### File: `src/components/tiles/LatestResultsTile.tsx`
|
||||
|
||||
**Layout:** Half-width card (single grid column). Sits in the LEFT column.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: teal dot + "LATEST RESULTS" + right text "Updated May 2025"
|
||||
- 2×2 metric grid inside
|
||||
|
||||
**Metric Grid:**
|
||||
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 12px`
|
||||
|
||||
**Each Metric Card:**
|
||||
- `padding: 14px`, `border-radius: var(--radius-sm)` (6px)
|
||||
- `border: 1px solid var(--border-light)`, `background: var(--bg)` (#F0F5F4)
|
||||
- Value: 22px, 700 weight, `letter-spacing: -0.02em`, `line-height: 1.2`
|
||||
- Color by variant: green=#059669, amber=#D97706, teal=#0D6E6E
|
||||
- Label: 11px, text-secondary, 500 weight, `margin-top: 3px`
|
||||
- Sub: 10px, text-tertiary, mono font, `margin-top: 4px`
|
||||
|
||||
**Data:** `import { kpis } from '@/data/kpis'`
|
||||
|
||||
**KPI flip prep:** Each metric card should accept a `data-kpi-id` or an `onClick` prop placeholder — Task 17 will add the flip interaction. For now, render as static display.
|
||||
|
||||
**Values:**
|
||||
| Value | Label | Sub | Color |
|
||||
|-------|-------|-----|-------|
|
||||
| £220M | Budget Oversight | NHS prescribing | green |
|
||||
| £14.6M | Efficiency Savings | Identified & tracked | amber |
|
||||
| 9+ | Years in NHS | Since 2016 | teal |
|
||||
| 12 | Team Size Led | Cross-functional | green |
|
||||
|
||||
---
|
||||
|
||||
## Task 11: CoreSkills Tile ("Repeat Medications")
|
||||
|
||||
### File: `src/components/tiles/CoreSkillsTile.tsx`
|
||||
|
||||
**Layout:** Half-width card (single grid column). Sits in the RIGHT column, next to LatestResults.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: amber dot + "REPEAT MEDICATIONS"
|
||||
- Vertical list of skill items, `gap: 10px`
|
||||
|
||||
**Each Skill Item:**
|
||||
Matches the concept's `.dev-item` pattern:
|
||||
- `display: flex`, `align-items: center`, `gap: 10px`
|
||||
- 12.5px font, `padding: 10px 12px`
|
||||
- `background: var(--bg)` (#F0F5F4), `border-radius: var(--radius-sm)` (6px)
|
||||
- `border: 1px solid var(--border-light)`
|
||||
|
||||
**Item structure:**
|
||||
- **Icon container** (28px square, 6px radius):
|
||||
- `background: var(--accent-light)`, `color: var(--accent)` (teal)
|
||||
- Lucide icon inside (14px): `BarChart3` for Data Analysis, `Code2` for Python, `Database` for SQL, `PieChart` for Power BI, `FileCode2` for JS/TS
|
||||
- **Text block** (flex: 1):
|
||||
- Name: 600 weight, text-primary (e.g., "Data Analysis")
|
||||
- Frequency + years: 11px, text-tertiary, mono font (e.g., "Twice daily · Since 2016 · 9 yrs")
|
||||
- **Optional status badge**: 10px, 500 weight, pill shape (padding 3px 8px, border-radius 20px), flex-shrink-0
|
||||
- Could show proficiency or "Active" status
|
||||
|
||||
**Medication metaphor format:**
|
||||
```
|
||||
[📊] Data Analysis Active
|
||||
Twice daily · Since 2016 · 9 yrs
|
||||
|
||||
[💻] Python Active
|
||||
Daily · Since 2019 · 6 yrs
|
||||
|
||||
[🗄️] SQL Active
|
||||
Daily · Since 2018 · 7 yrs
|
||||
|
||||
[📈] Power BI Active
|
||||
Once weekly · Since 2020 · 5 yrs
|
||||
|
||||
[📝] JavaScript / TypeScript Active
|
||||
When required · Since 2022 · 3 yrs
|
||||
```
|
||||
|
||||
**Data:** `import { skills } from '@/data/skills'`
|
||||
|
||||
**Expansion prep:** Each item should accept an onClick prop placeholder — Task 16 will add expansion to show prescribing history (from existing medications data).
|
||||
@@ -0,0 +1,204 @@
|
||||
# Reference: Tasks 12-15 — Bottom Tiles
|
||||
|
||||
## Task 12: LastConsultation Tile
|
||||
|
||||
### File: `src/components/tiles/LastConsultationTile.tsx`
|
||||
|
||||
**Layout:** Full-width card.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: green dot + "LAST CONSULTATION" + right text "Most recent role"
|
||||
|
||||
**Header info row:**
|
||||
- `display: flex`, `flex-wrap: wrap`, `gap: 16px`
|
||||
- `margin-bottom: 14px`, `padding-bottom: 14px`, `border-bottom: 1px solid var(--border-light)`
|
||||
- Each field:
|
||||
- Label: 10px, uppercase, `letter-spacing: 0.06em`, text-tertiary
|
||||
- Value: 11.5px, 600 weight, text-primary
|
||||
|
||||
| Label | Value |
|
||||
|-------|-------|
|
||||
| Date | May 2025 |
|
||||
| Organisation | NHS Norfolk & Waveney ICB |
|
||||
| Type | Permanent · Full-time |
|
||||
| Band | 8a |
|
||||
|
||||
**Role title:**
|
||||
- "Interim Head, Population Health & Data Analysis"
|
||||
- 13.5px, 600 weight, `color: var(--accent)` (#0D6E6E)
|
||||
- `margin-bottom: 12px`
|
||||
|
||||
**Bullet list:**
|
||||
- `list-style: none`, flex column, `gap: 7px`
|
||||
- Each bullet: 12.5px, text-primary, `padding-left: 16px`, `line-height: 1.5`
|
||||
- Pseudo `::before`: 5px circle, accent color (#0D6E6E), `opacity: 0.5`, positioned left at top 7px
|
||||
|
||||
**Bullets** (from first consultation's examination array):
|
||||
- Led a cross-functional team of 12 across data, analytics, and population health workstreams
|
||||
- Oversaw £220M prescribing budget with full analytical accountability and reporting to ICB board
|
||||
- Identified £14.6M in efficiency savings through data-driven prescribing interventions
|
||||
- Designed and deployed Power BI dashboards used by 200+ clinicians and commissioners
|
||||
- Spearheaded SQL analytics transformation, migrating legacy Access databases to modern data stack
|
||||
- Established team data literacy programme, upskilling 30+ non-technical staff in data interpretation
|
||||
|
||||
**Data:** `import { consultations } from '@/data/consultations'` — use `consultations[0]` (the most recent).
|
||||
|
||||
Map consultation fields:
|
||||
- date → Date field
|
||||
- organization → Organisation field
|
||||
- role → Role title
|
||||
- examination array → Bullet points
|
||||
|
||||
---
|
||||
|
||||
## Task 13: CareerActivity Tile
|
||||
|
||||
### File: `src/components/tiles/CareerActivityTile.tsx`
|
||||
|
||||
**Layout:** Full-width card.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: teal dot + "CAREER ACTIVITY" + right text "Full timeline"
|
||||
|
||||
**Activity grid:**
|
||||
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 10px`
|
||||
- Below 900px: `grid-template-columns: 1fr` (single column)
|
||||
|
||||
**Each activity item:**
|
||||
- `display: flex`, `gap: 10px`
|
||||
- `padding: 10px 12px`
|
||||
- `background: var(--bg)` (#F0F5F4)
|
||||
- `border-radius: var(--radius-sm)` (6px)
|
||||
- `border: 1px solid var(--border-light)`
|
||||
- 12px font
|
||||
- `transition: border-color 0.15s`
|
||||
- Hover: `border-color: var(--accent-border)`
|
||||
|
||||
**Dot (left):**
|
||||
- 8px circle, flex-shrink-0, `margin-top: 2px` (aligns with text)
|
||||
- Color by type:
|
||||
- Role: teal (#0D6E6E)
|
||||
- Project: amber (#D97706)
|
||||
- Certification: green (#059669)
|
||||
- Education: purple (#7C3AED)
|
||||
|
||||
**Content (right):**
|
||||
- Title: 600 weight, text-primary, `line-height: 1.3`
|
||||
- Meta: 11px, text-secondary, `margin-top: 2px`
|
||||
- Date: 10px, mono font, text-tertiary, `margin-top: 3px`
|
||||
|
||||
**Building the timeline data:**
|
||||
|
||||
Merge entries from multiple data sources, sorted newest-first:
|
||||
|
||||
```typescript
|
||||
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
|
||||
|
||||
interface ActivityEntry {
|
||||
id: string
|
||||
type: ActivityType
|
||||
title: string
|
||||
meta: string
|
||||
date: string
|
||||
sortYear: number // for sorting
|
||||
}
|
||||
```
|
||||
|
||||
Sources:
|
||||
1. `consultations` → type "role": title=role, meta=organization, date=duration
|
||||
2. `investigations` (selected key ones) → type "project": title=name, meta=short description, date=year
|
||||
3. `documents` where type='Certificate' → type "cert": title=title, meta=source, date=date
|
||||
4. `documents` where type='Results' (MPharm) → type "edu": title=title, meta=source, date=date
|
||||
|
||||
Match the concept HTML entries:
|
||||
| Type | Title | Meta | Date |
|
||||
|------|-------|------|------|
|
||||
| role | Interim Head, Population Health & Data Analysis | NHS Norfolk & Waveney ICB | 2024 – 2025 |
|
||||
| project | £220M Prescribing Budget Oversight | Lead analyst & budget owner | 2024 |
|
||||
| role | Senior Data Analyst — Medicines Optimisation | NHS Norfolk & Waveney ICB | 2021 – 2024 |
|
||||
| project | SQL Analytics Transformation | Legacy migration project lead | 2025 |
|
||||
| cert | Power BI Data Analyst Associate | Microsoft Certified | 2023 |
|
||||
| role | Prescribing Data Pharmacist | NHS Norwich CCG | 2018 – 2021 |
|
||||
| cert | Clinical Pharmacy Diploma | Professional development | 2019 |
|
||||
| role | Community Pharmacist | Boots UK | 2016 – 2018 |
|
||||
| edu | MPharm (Hons) — 2:1 | University of East Anglia | 2011 – 2015 |
|
||||
| cert | GPhC Registration | General Pharmaceutical Council | August 2016 |
|
||||
|
||||
**Expansion prep:** Activity items should accept onClick for Task 16 (expand to show full role/project detail).
|
||||
|
||||
---
|
||||
|
||||
## Task 14: Education Tile
|
||||
|
||||
### File: `src/components/tiles/EducationTile.tsx`
|
||||
|
||||
**Layout:** Full-width card, below Career Activity.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: purple dot (#7C3AED) + "EDUCATION"
|
||||
|
||||
**Education entries:**
|
||||
Vertical stack of education items.
|
||||
|
||||
Each item:
|
||||
- `padding: 7px 10px`
|
||||
- `background: var(--surface)` (#FFFFFF)
|
||||
- `border: 1px solid var(--border-light)`
|
||||
- `border-radius: var(--radius-sm)` (6px)
|
||||
- 11.5px, text-primary
|
||||
|
||||
Structure:
|
||||
- Degree name: 600 weight, `display: block`
|
||||
- Detail: text-secondary, 11px, `margin-top: 2px`
|
||||
|
||||
**Entries** (from CV):
|
||||
| Degree | Detail |
|
||||
|--------|--------|
|
||||
| MPharm (Hons) — 2:1 | University of East Anglia · 2015 |
|
||||
| NHS Leadership Academy — Mary Seacole Programme | 2018 · 78% |
|
||||
| A-Levels: Mathematics (A*), Chemistry (B), Politics (C) | Highworth Grammar School · 2009–2011 |
|
||||
|
||||
**Data:** Filter `src/data/documents.ts` for education entries, or hardcode from CV since the documents data may not have all education entries.
|
||||
|
||||
Note: The concept HTML only shows the MPharm entry. But the CV has more education. Include all CV education entries.
|
||||
|
||||
---
|
||||
|
||||
## Task 15: Projects Tile
|
||||
|
||||
### File: `src/components/tiles/ProjectsTile.tsx`
|
||||
|
||||
**Layout:** Full-width card, prominent position.
|
||||
|
||||
**Content:**
|
||||
- CardHeader: amber dot + "ACTIVE PROJECTS"
|
||||
|
||||
**Project entries:**
|
||||
Vertical list, styled as interactive items.
|
||||
|
||||
Each project:
|
||||
- `display: flex`, `align-items: flex-start`, `gap: 8px`
|
||||
- `padding: 7px 10px`
|
||||
- `background: var(--surface)`, `border: 1px solid var(--border-light)`
|
||||
- `border-radius: var(--radius-sm)` (6px)
|
||||
- 11.5px, text-primary
|
||||
- Hover: `border-color: var(--accent-border)`
|
||||
- `transition: border-color 0.15s`
|
||||
|
||||
Structure:
|
||||
- **Status dot** (7px circle, flex-shrink-0, `margin-top: 4px`):
|
||||
- Complete: success (#059669)
|
||||
- Ongoing: accent (#0D6E6E)
|
||||
- Live: success with pulse animation
|
||||
- **Project name**: text-primary, flex 1
|
||||
- **Year badge**: 10px, mono font, text-tertiary, `margin-left: auto`, flex-shrink-0
|
||||
|
||||
**Data:** `import { investigations } from '@/data/investigations'`
|
||||
|
||||
Map investigations to projects:
|
||||
- name → Project name
|
||||
- status → dot color
|
||||
- requestedYear → Year badge
|
||||
- resultSummary → Available for expansion (Task 16)
|
||||
|
||||
**Expansion prep:** Each item should accept onClick for Task 16 (expand to show methodology, tech stack, results).
|
||||
@@ -0,0 +1,248 @@
|
||||
# Reference: Tasks 16-18 — Interactions
|
||||
|
||||
## Task 16: Tile Expansion System
|
||||
|
||||
### Overview
|
||||
|
||||
Three tiles have expandable items: CareerActivity (roles), Projects, and CoreSkills. Clicking an item expands it in-place to reveal detail, like expanding a clinical record entry.
|
||||
|
||||
### Expansion Pattern (consistent across all tiles)
|
||||
|
||||
**Animation:**
|
||||
- Framer Motion `AnimatePresence` + `motion.div`
|
||||
- Height-only animation: 200ms, ease-out
|
||||
- **No opacity fade on content** (guardrail)
|
||||
- `overflow: hidden` on the animated container
|
||||
|
||||
```typescript
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 'auto' }}
|
||||
exit={{ height: 0 }}
|
||||
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{/* expanded content */}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Single-expand accordion: only one item expanded at a time within each tile
|
||||
- Click expanded item again to collapse
|
||||
- Click different item: collapses current, expands new
|
||||
- State: `expandedItemId: string | null` in each tile component
|
||||
|
||||
**Keyboard:**
|
||||
- Enter/Space: toggle expand/collapse
|
||||
- Escape: collapse current item
|
||||
- `aria-expanded` on each clickable item
|
||||
|
||||
**Visual:**
|
||||
- Expanded content has slightly different background (`var(--bg)` or subtle border-left)
|
||||
- Colored left border on expanded panel (accent color for roles, amber for projects, teal for skills)
|
||||
- Content padding: 12-16px
|
||||
|
||||
### CareerActivity Expansion (roles)
|
||||
|
||||
When a role-type activity item is expanded:
|
||||
- Show full role details from corresponding consultation entry
|
||||
- Structure: role title, organization, date range
|
||||
- Achievement bullets (examination array from consultation)
|
||||
- Coded entries if available
|
||||
- Match expanded content to `consultations` data by mapping activity item to consultation
|
||||
|
||||
### Projects Expansion
|
||||
|
||||
When a project item is expanded:
|
||||
- Show from investigation data:
|
||||
- Methodology
|
||||
- Tech stack (as tags or inline list)
|
||||
- Results (bulleted)
|
||||
- External URL link if available ("View Results" button)
|
||||
|
||||
### CoreSkills Expansion
|
||||
|
||||
When a skill item is expanded:
|
||||
- Show "prescribing history" — a timeline of skill development
|
||||
- Source: Can use the existing `medications` data which has `prescribingHistory` entries
|
||||
- Format: vertical timeline with year markers and descriptions
|
||||
- Timeline dots: accent color, 6px, with connecting line
|
||||
- Year: mono font, 12px, semibold
|
||||
- Description: 12px, regular
|
||||
|
||||
---
|
||||
|
||||
## Task 17: KPI Flip Cards
|
||||
|
||||
### Overview
|
||||
|
||||
In the LatestResults tile, each metric card can be clicked to "flip" and reveal an explanation of that KPI.
|
||||
|
||||
### Flip Animation
|
||||
|
||||
**CSS Perspective approach:**
|
||||
```css
|
||||
.metric-card {
|
||||
perspective: 1000px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metric-card-inner {
|
||||
transition: transform 0.4s ease-in-out;
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metric-card-inner.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.metric-card-front,
|
||||
.metric-card-back {
|
||||
backface-visibility: hidden;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.metric-card-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
```
|
||||
|
||||
Or use Framer Motion `animate={{ rotateY: isFlipped ? 180 : 0 }}` with `perspective` on parent.
|
||||
|
||||
**Behavior:**
|
||||
- Click to flip front → back
|
||||
- Click again to flip back → front
|
||||
- Only one card flipped at a time (clicking another card flips the current one back)
|
||||
- State: `flippedCardId: string | null` in LatestResultsTile
|
||||
|
||||
**Front face:** Current metric display (value + label + sub) — same as Task 10.
|
||||
|
||||
**Back face:**
|
||||
- `background: var(--accent-light)` (subtle teal tint)
|
||||
- `padding: 14px`
|
||||
- Text: 12px, text-secondary, `line-height: 1.5`
|
||||
- The explanation text from KPI data's `explanation` field
|
||||
|
||||
**Reduced motion:**
|
||||
- No 3D flip animation
|
||||
- Instant content swap (front → back)
|
||||
- Could use a simple crossfade or just replace content immediately
|
||||
|
||||
**Keyboard:**
|
||||
- Enter/Space to flip
|
||||
- Each metric card should be `tabIndex={0}` with appropriate `aria-label`
|
||||
|
||||
**KPI Explanations** (from `src/data/kpis.ts`):
|
||||
- £220M: Budget management with forecasting models
|
||||
- £14.6M: Efficiency programme through data analysis
|
||||
- 9+ Years: NHS service progression since 2016
|
||||
- 12: Cross-functional team leadership
|
||||
|
||||
---
|
||||
|
||||
## Task 18: Command Palette
|
||||
|
||||
### File: `src/components/CommandPalette.tsx`
|
||||
|
||||
### Trigger
|
||||
- **Ctrl+K** (global `keydown` listener on `document`)
|
||||
- **Click** on TopBar search bar (or focus on search input)
|
||||
- The TopBar search input does NOT do inline search — it opens the palette
|
||||
|
||||
### Overlay
|
||||
- `position: fixed`, `inset: 0`
|
||||
- `background: rgba(26,43,42,0.45)`
|
||||
- `backdrop-filter: blur(4px)`
|
||||
- `z-index: 1000`
|
||||
- Fade in: `opacity: 0 → 1`, `visibility: hidden → visible`, 200ms transition
|
||||
- Click overlay (outside modal) to close
|
||||
|
||||
### Palette Modal
|
||||
- `width: 580px`, `max-height: 520px`
|
||||
- `background: var(--surface)` (#FFFFFF)
|
||||
- `border-radius: 12px`
|
||||
- `box-shadow: 0 20px 60px rgba(26,43,42,0.2), 0 0 0 1px rgba(26,43,42,0.08)`
|
||||
- `overflow: hidden`
|
||||
- Entrance: `transform: scale(0.97) translateY(-8px)` → `scale(1) translateY(0)`, 200ms cubic-bezier
|
||||
|
||||
### Search Input
|
||||
- Flex row: search icon (18px, accent) + input + "ESC" hint badge
|
||||
- `padding: 14px 18px`, `border-bottom: 1px solid var(--border-light)`
|
||||
- Input: 15px, font-body, placeholder "Search records, experience, skills..."
|
||||
- ESC badge: mono 10px, tertiary, bg var(--bg), border, padding 2px 7px, radius 4px
|
||||
|
||||
### Results Area
|
||||
- `overflow-y: auto`, `padding: 8px`, `flex: 1`
|
||||
- Custom scrollbar (4px)
|
||||
|
||||
### Result Sections
|
||||
Section label: 10px, 600 weight, uppercase, `letter-spacing: 0.08em`, text-tertiary, `padding: 8px 10px 5px`
|
||||
|
||||
### Result Items
|
||||
- `display: flex`, `align-items: center`, `gap: 10px`
|
||||
- `padding: 9px 10px`, `border-radius: var(--radius-sm)` (6px)
|
||||
- `cursor: pointer`, `transition: background 0.1s`
|
||||
- 13px, text-primary
|
||||
- Hover/selected: `background: var(--accent-light)`
|
||||
- Selected also gets: `outline: 1.5px solid var(--accent-border)`
|
||||
|
||||
**Item structure:**
|
||||
- Icon container: 28px square, 6px radius, colored bg per section
|
||||
- Experience: teal
|
||||
- Core Skills: green
|
||||
- Active Projects: amber
|
||||
- Achievements: amber
|
||||
- Education: purple
|
||||
- Quick Actions: teal
|
||||
- Text: title (500 weight) + subtitle (11px, tertiary, truncated)
|
||||
- Optional badge: 10px, mono, tertiary
|
||||
|
||||
### Fuzzy Search
|
||||
|
||||
Adapt existing `src/lib/search.ts` (fuse.js integration):
|
||||
- Rebuild search index to include new data (skills from skills.ts, KPIs, etc.)
|
||||
- `threshold: 0.3`, weighted keys (title: 2, content: 1)
|
||||
- `minMatchCharLength: 2`
|
||||
- Group results by section
|
||||
- Highlight matching text in titles using `<mark>` with accent-light background
|
||||
|
||||
### Keyboard Navigation
|
||||
- **Arrow Down/Up**: move selection through results
|
||||
- **Enter**: select highlighted result (navigate to section or trigger action)
|
||||
- **Escape**: close palette
|
||||
- `selectedIndex` state tracks which result is highlighted
|
||||
- Auto-scroll highlighted result into view
|
||||
|
||||
### Quick Actions Section
|
||||
| Title | Subtitle | Action |
|
||||
|-------|----------|--------|
|
||||
| Download CV | Export as PDF | Trigger download |
|
||||
| Send Email | andy@charlwood.xyz | `mailto:` link |
|
||||
| View LinkedIn | Professional profile | External link |
|
||||
| View Projects | GitHub & portfolio | External link |
|
||||
|
||||
### Footer
|
||||
- `display: flex`, `gap: 12px`
|
||||
- `padding: 10px 18px`, `border-top: 1px solid var(--border-light)`
|
||||
- 11px, text-tertiary
|
||||
- Keyboard hints: `↑ ↓ Navigate`, `Enter Select`, `Esc Close`
|
||||
- Each key in `<kbd>` styled element
|
||||
|
||||
### Reduced Motion
|
||||
- No scale/translate entrance animation
|
||||
- Instant show/hide (opacity only, or immediate)
|
||||
|
||||
### State Management
|
||||
```typescript
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
```
|
||||
|
||||
Render the palette at the DashboardLayout level so it overlays everything.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Reference: Tasks 19-21 — Polish
|
||||
|
||||
## Task 19: Responsive Design
|
||||
|
||||
### Desktop (>1024px)
|
||||
- Full sidebar (272px) + TopBar + 2-column card grid
|
||||
- All tiles at full spec (as designed in Tasks 8-15)
|
||||
- Command palette at 580px width
|
||||
|
||||
### Tablet (768–1024px)
|
||||
- Sidebar: collapse to icon-only (56px) or hide entirely with toggle
|
||||
- TopBar: full, but search bar may shrink (reduce min-width)
|
||||
- Card grid: can stay 2-column if space permits, or switch to 1-column
|
||||
- Activity grid inside CareerActivity tile: switch to 1-column
|
||||
|
||||
### Mobile (<768px)
|
||||
- Sidebar: hidden entirely (off-canvas or removed)
|
||||
- TopBar: simplified — brand text may truncate, hide search bar center section
|
||||
- Navigation: consider a hamburger menu or bottom nav for key actions
|
||||
- Card grid: single column
|
||||
- All tiles stack vertically (full-width)
|
||||
- Metric grid in LatestResults: stays 2x2 (compact enough)
|
||||
- Activity grid in CareerActivity: single column
|
||||
- Touch targets: all clickable elements 48px+ minimum
|
||||
- Command palette: full-width with reduced padding
|
||||
|
||||
### Breakpoint Strategy
|
||||
Use Tailwind responsive prefixes:
|
||||
- `lg:` for desktop (>1024px)
|
||||
- `md:` for tablet (>768px)
|
||||
- Default styles for mobile-first
|
||||
|
||||
### Key responsive classes:
|
||||
```
|
||||
/* Card grid */
|
||||
grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-[16px]
|
||||
|
||||
/* Sidebar visibility */
|
||||
hidden lg:flex lg:flex-col
|
||||
|
||||
/* TopBar search */
|
||||
hidden md:block
|
||||
|
||||
/* Activity grid */
|
||||
grid grid-cols-1 md:grid-cols-2
|
||||
|
||||
/* Sidebar width */
|
||||
lg:w-[272px] lg:min-w-[272px]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 20: Accessibility Audit
|
||||
|
||||
### Semantic HTML
|
||||
| Element | Tag | Notes |
|
||||
|---------|-----|-------|
|
||||
| TopBar | `<header>` | Fixed at top |
|
||||
| Sidebar | `<aside>` or `<nav>` | Navigation/info panel |
|
||||
| Main content | `<main>` | Card grid container |
|
||||
| Individual tiles | `<article>` | Self-contained content sections |
|
||||
| Tile sections | `<section>` | Within tiles (e.g., metric grid, bullet list) |
|
||||
| Command palette | `<dialog>` or `div role="dialog"` | Modal overlay |
|
||||
|
||||
### Keyboard Navigation
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Move between interactive elements (tiles, buttons, links) |
|
||||
| Enter/Space | Expand tile items, flip KPI cards, select palette results |
|
||||
| Escape | Close expanded items, close command palette |
|
||||
| Ctrl+K | Open command palette |
|
||||
| Arrow Up/Down | Navigate command palette results |
|
||||
|
||||
### ARIA Attributes
|
||||
- **Command palette search**: `role="combobox"`, `aria-expanded`, `aria-controls="palette-results"`, `aria-autocomplete="list"`
|
||||
- **Palette results**: `role="listbox"`, each result `role="option"`
|
||||
- **Palette overlay**: `role="dialog"`, `aria-modal="true"`, `aria-label="Search records"`
|
||||
- **Expandable items**: `aria-expanded="true|false"` on trigger element
|
||||
- **KPI flip cards**: `aria-label` describing front/back content, `role="button"`, `tabIndex={0}`
|
||||
- **Status dots with text**: text labels present → dot can be `aria-hidden="true"`
|
||||
- **Alert flags**: `role="status"` or decorative (visible text is sufficient)
|
||||
- **Live region**: When palette opens/closes, announce via `aria-live="polite"` region
|
||||
- **TopBar session info**: `aria-label="Active session information"`
|
||||
|
||||
### Focus Management
|
||||
- **Command palette**: focus trap when open. Focus moves to search input on open. Returns to trigger element on close.
|
||||
- **Focus visible**: `focus-visible:ring-2 focus-visible:ring-[var(--accent)]/40` on all interactive elements (buttons, links, expandable items, KPI cards)
|
||||
- **Skip to content**: Optional "Skip to main content" link (only visible on focus)
|
||||
- **After tile expansion**: focus should remain on the trigger or move into expanded content
|
||||
|
||||
### `prefers-reduced-motion`
|
||||
Every animation must check:
|
||||
```typescript
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
```
|
||||
|
||||
| Animation | Reduced Motion Behavior |
|
||||
|-----------|------------------------|
|
||||
| Dashboard entrance (topbar/sidebar/content) | Instant, no slide/fade |
|
||||
| Tile expansion | Instant height change (duration: 0) |
|
||||
| KPI flip | Instant content swap (no rotateY) |
|
||||
| Palette entrance | Instant show (no scale/translate) |
|
||||
| Status badge pulse | No animation |
|
||||
| Hover transitions | Can keep (very brief) or disable |
|
||||
|
||||
### Color Contrast Verification
|
||||
| Foreground | Background | Expected Ratio | Meets AA? |
|
||||
|------------|-----------|-----------------|-----------|
|
||||
| #0D6E6E (accent) | #FFFFFF (white) | ~5.5:1 | Yes |
|
||||
| #1A2B2A (primary) | #FFFFFF | ~15:1 | Yes |
|
||||
| #5B7A78 (secondary) | #FFFFFF | ~4.6:1 | Borderline — verify |
|
||||
| #8DA8A5 (tertiary) | #FFFFFF | ~3.0:1 | Fails for body text — use only for decorative/supplementary |
|
||||
| #0D6E6E (accent) | #F0F5F4 (bg) | ~4.8:1 | Yes for large text |
|
||||
|
||||
**Important:** Tertiary text (#8DA8A5) does NOT meet AA for body text. Use only for supplementary labels, dates, and decorative text where the information is also conveyed elsewhere (e.g., a date that's also in the title). For standalone readable text, use secondary (#5B7A78) or primary (#1A2B2A).
|
||||
|
||||
---
|
||||
|
||||
## Task 21: Clean Up and Final Polish
|
||||
|
||||
### Components to Remove (only after confirming unused)
|
||||
- `src/components/PatientBanner.tsx` — replaced by TopBar
|
||||
- `src/components/ClinicalSidebar.tsx` — replaced by Sidebar
|
||||
- `src/components/Breadcrumb.tsx` — no longer needed (no view switching)
|
||||
- `src/components/MobileBottomNav.tsx` — may be replaced or redesigned
|
||||
- `src/components/PMRInterface.tsx` — replaced by DashboardLayout
|
||||
|
||||
### Views to Assess
|
||||
The `src/components/views/` directory contains the old view components. Some may be reusable:
|
||||
- **ConsultationsView.tsx**: Expanded entry rendering could be reused in CareerActivity expansion (Task 16). Check before removing.
|
||||
- **MedicationsView.tsx**: Prescribing history rendering could be reused in CoreSkills expansion. Check before removing.
|
||||
- **Other views**: If expansion (Task 16) doesn't reuse them, they can be removed.
|
||||
|
||||
**Rule: Only remove files that are confirmed unused.** Run a grep for imports before deleting.
|
||||
|
||||
### Hooks to Remove
|
||||
- `src/hooks/useScrollCondensation.ts` — only used by PatientBanner. If PatientBanner is removed, this can go too.
|
||||
|
||||
### Verification Checklist
|
||||
- [ ] No dead imports (run `npm run lint` — ESLint catches unused imports)
|
||||
- [ ] No TypeScript errors (`npm run typecheck`)
|
||||
- [ ] Clean build (`npm run build`)
|
||||
- [ ] Bundle size reasonable (should be similar to or smaller than current ~417KB)
|
||||
- [ ] No console errors in dev mode
|
||||
|
||||
### Final Visual Review
|
||||
Open `http://localhost:5173` and compare against `References/GPSystemconcept.html`:
|
||||
- [ ] TopBar layout matches (brand, search, session)
|
||||
- [ ] Sidebar matches (person header, tags, alerts)
|
||||
- [ ] Card grid layout (2-column, full-width tiles span both)
|
||||
- [ ] Each tile's visual treatment matches concept
|
||||
- [ ] Shadows, borders, radius consistent
|
||||
- [ ] Typography: Elvaro Grotesque (not DM Sans)
|
||||
- [ ] Colors: teal accent (not NHS Blue)
|
||||
- [ ] Hover states work (card shadow lift, border color change)
|
||||
- [ ] Responsive: test at 1280px, 800px, 375px widths
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user