Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3a72d0bee | |||
| 5a657c4aac | |||
| 78e994ec5e | |||
| 68f92fb9a0 | |||
| be7a65ef8a | |||
| 5fa01b8d66 | |||
| 98d767fa7f |
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
name: ralph-setup
|
||||||
|
description: Set up autonomous AI development tasks using the Ralph Wiggum technique. Use when the user wants to create a RALPH orchestration — either a simple looping prompt or a multi-hat coordinated workflow. Interviews the user to understand requirements, decides the appropriate mode, and generates all necessary configuration files (ralph.yml, hats.yml, PROMPT.md). Triggers on mentions of "ralph", "autonomous loop", "hat-based", "orchestration", or requests to set up iterative AI agent tasks.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ralph Setup Skill
|
||||||
|
|
||||||
|
Set up autonomous AI development tasks using the Ralph Wiggum technique — either as a simple iterating prompt or a coordinated hat-based workflow.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Ralph implements the Ralph Wiggum technique: give an AI agent a task, loop it until it's done. The orchestrator is deliberately thin — it trusts the agent to do the work and enforces quality through backpressure (tests, lint, typecheck must pass).
|
||||||
|
|
||||||
|
There are two modes:
|
||||||
|
|
||||||
|
| Mode | What It Does | Best For |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| **Traditional (Simple Prompt)** | Single loop — agent iterates until LOOP_COMPLETE | Quick tasks, single-concern work, anything one agent can handle in a straight line |
|
||||||
|
| **Hat-Based** | Specialised personas coordinate through typed events | Complex workflows, multi-step processes, tasks needing distinct planning/building/reviewing phases |
|
||||||
|
|
||||||
|
## Core Tenets (Apply to Both Modes)
|
||||||
|
|
||||||
|
These six tenets guide every RALPH setup. Reference them when making decisions:
|
||||||
|
|
||||||
|
1. **Fresh Context Is Reliability** — Each iteration clears context. The prompt must be self-contained enough to re-read, re-plan, and re-execute every cycle.
|
||||||
|
2. **Backpressure Over Prescription** — Don't prescribe HOW to do the work. Create gates that reject bad work (tests pass, lint clean, types check).
|
||||||
|
3. **The Plan Is Disposable** — Regeneration costs one planning loop. Cheap. Don't over-invest in preserving plans.
|
||||||
|
4. **Disk Is State, Git Is Memory** — Files are the handoff mechanism between iterations. Git provides checkpointing and rollback.
|
||||||
|
5. **Steer With Signals, Not Scripts** — Add signs (success criteria, quality gates), not step-by-step scripts.
|
||||||
|
6. **Let Ralph Ralph** — Sit ON the loop, not IN it. The orchestrator coordinates; the agent does the work.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Phase 1: Interview the User
|
||||||
|
|
||||||
|
Before generating anything, you need to understand the task. Ask targeted questions to fill in these blanks:
|
||||||
|
|
||||||
|
**Essential information:**
|
||||||
|
- What is the task? (Be specific — "build an API" is too vague; "build a REST API for user management with Express.js and TypeScript" is good)
|
||||||
|
- What does "done" look like? (Measurable success criteria — tests pass, endpoints respond, specific files exist)
|
||||||
|
- What language/framework/tools are involved?
|
||||||
|
- Does the project already exist, or is this greenfield?
|
||||||
|
- Are there existing tests, linting, or type-checking set up?
|
||||||
|
|
||||||
|
**Information that helps you decide the mode:**
|
||||||
|
- How many distinct phases or concerns does this task have? (1-2 = simple prompt; 3+ = consider hats)
|
||||||
|
- Does the task need planning before building? (If yes, hat-based is likely better)
|
||||||
|
- Does the task need a review/QA step separate from building? (If yes, hat-based)
|
||||||
|
- Is there a spec or design document to follow? (Spec-driven development suits hats well)
|
||||||
|
- How complex is the codebase? (Large existing codebase with multiple modules = hat-based)
|
||||||
|
|
||||||
|
**Don't over-interview.** If the user gives you a clear, well-scoped task, you may have enough after 1-2 questions. If the task is vague, probe until you can write a crisp PROMPT.md.
|
||||||
|
|
||||||
|
### Phase 2: Decide the Mode
|
||||||
|
|
||||||
|
Use this decision framework:
|
||||||
|
|
||||||
|
**Choose Simple Prompt when:**
|
||||||
|
- The task is a single concern (add a feature, fix a bug, write a script)
|
||||||
|
- One agent can handle it start to finish without distinct phases
|
||||||
|
- The success criteria are straightforward (tests pass, script runs)
|
||||||
|
- The user explicitly wants something quick and simple
|
||||||
|
- The task can be fully described in a PROMPT.md under ~50 lines
|
||||||
|
|
||||||
|
**Choose Hat-Based when:**
|
||||||
|
- The task has 3+ distinct phases (plan → build → test → review)
|
||||||
|
- Different phases need different "mindsets" (architect vs implementer vs reviewer)
|
||||||
|
- The task involves spec-driven development (spec → implement → verify)
|
||||||
|
- There's a TDD workflow (write tests → implement → verify)
|
||||||
|
- The task is large enough that a single prompt would be overwhelming
|
||||||
|
- Multiple files/modules need coordinated changes
|
||||||
|
- The user explicitly asks for hats or a structured workflow
|
||||||
|
|
||||||
|
**When in doubt:** Start with Simple Prompt. You can always add hats later. Simpler is more robust.
|
||||||
|
|
||||||
|
### Phase 3: Generate the Files
|
||||||
|
|
||||||
|
Generate the appropriate files into the user's project directory. Always explain what you're creating and why.
|
||||||
|
|
||||||
|
Read the appropriate reference file before generating:
|
||||||
|
- For Simple Prompt: `references/simple-prompt-reference.md`
|
||||||
|
- For Hat-Based: `references/hat-based-reference.md`
|
||||||
|
|
||||||
|
#### Files to Generate
|
||||||
|
|
||||||
|
**Both modes:**
|
||||||
|
- `ralph.yml` — Main configuration
|
||||||
|
- `PROMPT.md` — The task definition
|
||||||
|
|
||||||
|
**Hat-Based mode additionally:**
|
||||||
|
- `hats.yml` — Hat definitions with triggers, publishes, and instructions
|
||||||
|
|
||||||
|
### Phase 4: Review with the User
|
||||||
|
|
||||||
|
After generating the files, walk the user through what you created:
|
||||||
|
- Summarise the task as you understood it
|
||||||
|
- Explain the mode choice and why
|
||||||
|
- Highlight the success criteria / completion promise
|
||||||
|
- For hat-based: explain the event flow between hats
|
||||||
|
- Ask if anything needs adjusting before they run it
|
||||||
|
|
||||||
|
Then tell them how to run it:
|
||||||
|
```bash
|
||||||
|
# Simple prompt
|
||||||
|
ralph run
|
||||||
|
|
||||||
|
# Hat-based
|
||||||
|
ralph run --config hats.yml
|
||||||
|
|
||||||
|
# With iteration limit
|
||||||
|
ralph run --max-iterations 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Good Prompts (PROMPT.md)
|
||||||
|
|
||||||
|
The PROMPT.md is the most important file. It must be:
|
||||||
|
|
||||||
|
**Self-contained:** Every iteration starts fresh. The prompt must contain everything the agent needs to understand the task, check progress, and continue.
|
||||||
|
|
||||||
|
**Outcome-focused:** Define WHAT, not HOW. Let the agent figure out the approach.
|
||||||
|
|
||||||
|
**Measurable:** Include concrete success criteria the agent can verify:
|
||||||
|
- "All tests pass" (not "write good tests")
|
||||||
|
- "The /users endpoint returns 200 with valid JSON" (not "make the API work")
|
||||||
|
- "TypeScript compiles with zero errors" (not "fix the types")
|
||||||
|
|
||||||
|
**Structured but not prescriptive:** Use sections like Task, Requirements, Success Criteria, Constraints. Don't write step-by-step instructions.
|
||||||
|
|
||||||
|
### Prompt Template (Simple)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Task: [Clear, specific title]
|
||||||
|
|
||||||
|
[2-3 sentence description of what needs to be built/done]
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Specific requirement 1]
|
||||||
|
- [Specific requirement 2]
|
||||||
|
- [Specific requirement 3]
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
All of the following must be true:
|
||||||
|
- [ ] [Measurable criterion 1]
|
||||||
|
- [ ] [Measurable criterion 2]
|
||||||
|
- [ ] [Measurable criterion 3]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- [Technology constraints]
|
||||||
|
- [Style/convention constraints]
|
||||||
|
- [Performance constraints if any]
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Track your progress here. Mark items complete as you go.
|
||||||
|
When all success criteria are met, print LOOP_COMPLETE.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designing Hat Systems
|
||||||
|
|
||||||
|
When creating hats, follow these principles:
|
||||||
|
|
||||||
|
**Each hat should have a single responsibility.** Don't create a hat that plans AND builds.
|
||||||
|
|
||||||
|
**Events flow forward.** The event chain should be a clear pipeline: task.start → plan.ready → build.done → review.complete → task.done.
|
||||||
|
|
||||||
|
**Instructions should be specific to the hat's role.** The planner hat gets planning instructions, the builder gets building instructions.
|
||||||
|
|
||||||
|
**Keep it minimal.** 2-4 hats is typical. More than 5 is usually overengineered.
|
||||||
|
|
||||||
|
### Common Hat Patterns
|
||||||
|
|
||||||
|
**Plan → Build (2 hats):**
|
||||||
|
Good for tasks that need architectural thinking before coding.
|
||||||
|
|
||||||
|
**Plan → Build → Review (3 hats):**
|
||||||
|
Good for tasks that need quality assurance.
|
||||||
|
|
||||||
|
**Spec → Implement → Verify (3 hats):**
|
||||||
|
Good for spec-driven development.
|
||||||
|
|
||||||
|
**Test → Implement → Verify (3 hats):**
|
||||||
|
Good for TDD workflows.
|
||||||
|
|
||||||
|
See `references/hat-based-reference.md` for full configuration examples.
|
||||||
|
|
||||||
|
## Backpressure Configuration
|
||||||
|
|
||||||
|
Backpressure gates reject incomplete work. Common gates:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backpressure:
|
||||||
|
gates:
|
||||||
|
- name: "tests"
|
||||||
|
command: "npm test"
|
||||||
|
on_fail: "retry"
|
||||||
|
- name: "lint"
|
||||||
|
command: "npm run lint"
|
||||||
|
on_fail: "retry"
|
||||||
|
- name: "typecheck"
|
||||||
|
command: "npx tsc --noEmit"
|
||||||
|
on_fail: "retry"
|
||||||
|
```
|
||||||
|
|
||||||
|
Only add gates for tools that exist in the project. If there are no tests yet, don't add a test gate (unless the task IS to create tests).
|
||||||
|
|
||||||
|
## Cost and Safety
|
||||||
|
|
||||||
|
Always configure iteration limits. Remind the user:
|
||||||
|
- Default max iterations: 100
|
||||||
|
- Default max runtime: 4 hours
|
||||||
|
- A 50-iteration cycle on a large codebase can cost $50-100+ in API credits
|
||||||
|
- Recommend starting with `--max-iterations 30` for new setups and increasing if needed
|
||||||
|
- Git checkpointing is on by default — the user can always roll back
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# Hat-Based Reference
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Hat-based mode uses specialised personas ("hats") that coordinate through typed events. Each hat triggers on specific events and publishes new events when done, creating a pipeline of distinct phases.
|
||||||
|
|
||||||
|
Use this when the task genuinely benefits from separating concerns — e.g., planning separately from building, or reviewing separately from implementing.
|
||||||
|
|
||||||
|
## hats.yml Structure
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
starting_event: "task.start" # First event that kicks off the pipeline
|
||||||
|
completion_promise: "LOOP_COMPLETE" # String that signals completion
|
||||||
|
max_iterations: 100 # Safety limit
|
||||||
|
|
||||||
|
hats:
|
||||||
|
hat_name:
|
||||||
|
name: "Human-Readable Name"
|
||||||
|
triggers: ["event.that.activates.this.hat"]
|
||||||
|
publishes: ["event.this.hat.emits.when.done"]
|
||||||
|
instructions: |
|
||||||
|
Detailed instructions for what this hat should do.
|
||||||
|
Must be self-contained — the hat gets fresh context each time.
|
||||||
|
Should reference PROMPT.md for the overall task.
|
||||||
|
Should specify what "done" means for this hat.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- **triggers**: List of events that activate this hat. A hat runs when ANY of its trigger events fire.
|
||||||
|
- **publishes**: List of events this hat emits when it completes its work.
|
||||||
|
- **instructions**: The prompt for this hat. Must be specific to the hat's role.
|
||||||
|
- Events flow forward through the pipeline. Avoid circular event chains.
|
||||||
|
- The last hat in the pipeline should print LOOP_COMPLETE when the overall task is done.
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Plan → Build (2 Hats)
|
||||||
|
|
||||||
|
Best for tasks that need architectural thinking before coding.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
starting_event: "task.start"
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
|
||||||
|
hats:
|
||||||
|
planner:
|
||||||
|
name: "Planner"
|
||||||
|
triggers: ["task.start"]
|
||||||
|
publishes: ["plan.ready"]
|
||||||
|
instructions: |
|
||||||
|
You are the Planner. Read PROMPT.md to understand the task.
|
||||||
|
|
||||||
|
Your job:
|
||||||
|
1. Analyse the requirements and existing codebase
|
||||||
|
2. Create a clear implementation plan in .ralph/plan.md
|
||||||
|
3. Break the work into concrete steps with file-level detail
|
||||||
|
4. Identify any risks or unknowns
|
||||||
|
|
||||||
|
Write the plan to .ralph/plan.md then emit plan.ready.
|
||||||
|
|
||||||
|
Do NOT write any code. Planning only.
|
||||||
|
|
||||||
|
builder:
|
||||||
|
name: "Builder"
|
||||||
|
triggers: ["plan.ready"]
|
||||||
|
publishes: ["task.done"]
|
||||||
|
instructions: |
|
||||||
|
You are the Builder. Read PROMPT.md for the task and .ralph/plan.md
|
||||||
|
for the implementation plan.
|
||||||
|
|
||||||
|
Your job:
|
||||||
|
1. Follow the plan step by step
|
||||||
|
2. Write clean, tested code
|
||||||
|
3. Run tests after each significant change
|
||||||
|
4. Update .ralph/plan.md to mark completed steps
|
||||||
|
|
||||||
|
When all success criteria from PROMPT.md are met and all tests pass,
|
||||||
|
print LOOP_COMPLETE.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Plan → Build → Review (3 Hats)
|
||||||
|
|
||||||
|
Adds a review phase for quality assurance.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
starting_event: "task.start"
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
|
||||||
|
hats:
|
||||||
|
planner:
|
||||||
|
name: "Planner"
|
||||||
|
triggers: ["task.start", "review.changes_requested"]
|
||||||
|
publishes: ["plan.ready"]
|
||||||
|
instructions: |
|
||||||
|
You are the Planner. Read PROMPT.md to understand the task.
|
||||||
|
|
||||||
|
If triggered by review.changes_requested, read .ralph/review.md
|
||||||
|
for feedback and update the plan accordingly.
|
||||||
|
|
||||||
|
Create or update .ralph/plan.md with a clear implementation plan.
|
||||||
|
Emit plan.ready when done. Do NOT write code.
|
||||||
|
|
||||||
|
builder:
|
||||||
|
name: "Builder"
|
||||||
|
triggers: ["plan.ready"]
|
||||||
|
publishes: ["build.done"]
|
||||||
|
instructions: |
|
||||||
|
You are the Builder. Read PROMPT.md and .ralph/plan.md.
|
||||||
|
|
||||||
|
Implement the plan. Write tests. Run them.
|
||||||
|
When implementation is complete, emit build.done.
|
||||||
|
|
||||||
|
Do NOT assess overall quality — that's the Reviewer's job.
|
||||||
|
|
||||||
|
reviewer:
|
||||||
|
name: "Reviewer"
|
||||||
|
triggers: ["build.done"]
|
||||||
|
publishes: ["review.approved", "review.changes_requested"]
|
||||||
|
instructions: |
|
||||||
|
You are the Reviewer. Read PROMPT.md for requirements.
|
||||||
|
|
||||||
|
Review the current state of the codebase against the success criteria:
|
||||||
|
1. Do all tests pass?
|
||||||
|
2. Are all requirements met?
|
||||||
|
3. Is the code clean and following project conventions?
|
||||||
|
4. Are there edge cases not covered?
|
||||||
|
|
||||||
|
If everything passes, write your review to .ralph/review.md
|
||||||
|
and print LOOP_COMPLETE.
|
||||||
|
|
||||||
|
If changes are needed, write specific feedback to .ralph/review.md
|
||||||
|
and emit review.changes_requested.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Spec → Implement → Verify (3 Hats)
|
||||||
|
|
||||||
|
For spec-driven development — good when working from a design document.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
starting_event: "task.start"
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
|
||||||
|
hats:
|
||||||
|
spec_writer:
|
||||||
|
name: "Spec Writer"
|
||||||
|
triggers: ["task.start", "verify.gaps_found"]
|
||||||
|
publishes: ["spec.ready"]
|
||||||
|
instructions: |
|
||||||
|
You are the Spec Writer. Read PROMPT.md for the high-level task.
|
||||||
|
|
||||||
|
If triggered by verify.gaps_found, read .ralph/verification.md
|
||||||
|
for gaps and update the spec to address them.
|
||||||
|
|
||||||
|
Write a detailed technical specification to .ralph/spec.md:
|
||||||
|
- API contracts (endpoints, request/response shapes)
|
||||||
|
- Data models
|
||||||
|
- Error handling behaviour
|
||||||
|
- Test scenarios
|
||||||
|
|
||||||
|
Emit spec.ready when done. Do NOT write implementation code.
|
||||||
|
|
||||||
|
implementer:
|
||||||
|
name: "Implementer"
|
||||||
|
triggers: ["spec.ready"]
|
||||||
|
publishes: ["implementation.done"]
|
||||||
|
instructions: |
|
||||||
|
You are the Implementer. Read .ralph/spec.md for the specification.
|
||||||
|
|
||||||
|
Implement exactly what the spec describes. Write tests that verify
|
||||||
|
each specification point. Run tests after each change.
|
||||||
|
|
||||||
|
Emit implementation.done when the spec is fully implemented.
|
||||||
|
|
||||||
|
verifier:
|
||||||
|
name: "Verifier"
|
||||||
|
triggers: ["implementation.done"]
|
||||||
|
publishes: ["verify.passed", "verify.gaps_found"]
|
||||||
|
instructions: |
|
||||||
|
You are the Verifier. Read .ralph/spec.md and PROMPT.md.
|
||||||
|
|
||||||
|
Verify that the implementation matches the spec:
|
||||||
|
1. Run all tests — they must pass
|
||||||
|
2. Check each spec point against the code
|
||||||
|
3. Verify success criteria from PROMPT.md
|
||||||
|
|
||||||
|
If everything checks out, print LOOP_COMPLETE.
|
||||||
|
|
||||||
|
If there are gaps, write them to .ralph/verification.md
|
||||||
|
and emit verify.gaps_found.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: TDD — Test → Implement → Verify (3 Hats)
|
||||||
|
|
||||||
|
For test-driven development workflows.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
starting_event: "task.start"
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
|
||||||
|
hats:
|
||||||
|
test_writer:
|
||||||
|
name: "Test Writer"
|
||||||
|
triggers: ["task.start", "verify.tests_needed"]
|
||||||
|
publishes: ["tests.ready"]
|
||||||
|
instructions: |
|
||||||
|
You are the Test Writer. Read PROMPT.md for requirements.
|
||||||
|
|
||||||
|
Write failing tests FIRST that describe the desired behaviour.
|
||||||
|
Tests should be comprehensive and cover edge cases.
|
||||||
|
|
||||||
|
If triggered by verify.tests_needed, read .ralph/verification.md
|
||||||
|
for the specific test gaps to fill.
|
||||||
|
|
||||||
|
Write tests, verify they fail (red phase), then emit tests.ready.
|
||||||
|
Do NOT write implementation code.
|
||||||
|
|
||||||
|
implementer:
|
||||||
|
name: "Implementer"
|
||||||
|
triggers: ["tests.ready"]
|
||||||
|
publishes: ["implementation.done"]
|
||||||
|
instructions: |
|
||||||
|
You are the Implementer. Your goal is to make the tests pass.
|
||||||
|
|
||||||
|
Read the test files to understand what behaviour is expected.
|
||||||
|
Write the minimum code to make all tests pass (green phase).
|
||||||
|
|
||||||
|
Run tests after each change. When all tests pass,
|
||||||
|
emit implementation.done.
|
||||||
|
|
||||||
|
verifier:
|
||||||
|
name: "Verifier"
|
||||||
|
triggers: ["implementation.done"]
|
||||||
|
publishes: ["verify.passed", "verify.tests_needed"]
|
||||||
|
instructions: |
|
||||||
|
You are the Verifier. Read PROMPT.md for the full requirements.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
1. All tests pass
|
||||||
|
2. Test coverage is adequate for the requirements
|
||||||
|
3. All success criteria from PROMPT.md are met
|
||||||
|
4. Code is clean (refactor phase if needed)
|
||||||
|
|
||||||
|
If complete, print LOOP_COMPLETE.
|
||||||
|
If more tests are needed, write gaps to .ralph/verification.md
|
||||||
|
and emit verify.tests_needed.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backpressure with Hats
|
||||||
|
|
||||||
|
Backpressure gates can be applied globally or per-hat:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Global backpressure — applies to all hats
|
||||||
|
backpressure:
|
||||||
|
gates:
|
||||||
|
- name: "tests"
|
||||||
|
command: "npm test"
|
||||||
|
on_fail: "retry"
|
||||||
|
- name: "lint"
|
||||||
|
command: "npm run lint"
|
||||||
|
on_fail: "retry"
|
||||||
|
|
||||||
|
# Per-hat backpressure
|
||||||
|
hats:
|
||||||
|
builder:
|
||||||
|
triggers: ["plan.ready"]
|
||||||
|
publishes: ["build.done"]
|
||||||
|
backpressure:
|
||||||
|
gates:
|
||||||
|
- name: "typecheck"
|
||||||
|
command: "npx tsc --noEmit"
|
||||||
|
on_fail: "retry"
|
||||||
|
instructions: |
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memories
|
||||||
|
|
||||||
|
Hats can use persistent memories stored in `.ralph/agent/memories.md`. These survive across iterations and sessions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hats:
|
||||||
|
builder:
|
||||||
|
memory:
|
||||||
|
path: ".ralph/agent/memories.md"
|
||||||
|
scope: "hat" # or "global" to share across hats
|
||||||
|
```
|
||||||
|
|
||||||
|
Memories are useful for capturing lessons learned, recording decisions, and avoiding repeated mistakes.
|
||||||
|
|
||||||
|
## Running Hat-Based Workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with hats config
|
||||||
|
ralph run --config hats.yml
|
||||||
|
|
||||||
|
# With iteration limit
|
||||||
|
ralph run --config hats.yml --max-iterations 50
|
||||||
|
|
||||||
|
# Resume interrupted session
|
||||||
|
ralph run --config hats.yml --continue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
**Too many hats.** If you have more than 5, you're probably overengineering. Each hat adds coordination overhead.
|
||||||
|
|
||||||
|
**Circular event chains without an exit.** Every cycle must have a path to LOOP_COMPLETE. If planner → builder → reviewer → planner, the reviewer must sometimes emit completion instead of always cycling back.
|
||||||
|
|
||||||
|
**Hats that duplicate work.** If the builder is also doing planning, your planner hat is wasted.
|
||||||
|
|
||||||
|
**Overly prescriptive hat instructions.** The instructions should say WHAT to achieve, not HOW. Let the agent figure out the approach.
|
||||||
|
|
||||||
|
**Missing the PROMPT.md reference.** Hat instructions should always tell the agent to read PROMPT.md for the overall task context. Without it, hats lose sight of the bigger picture.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Simple Prompt Reference
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Traditional mode is Ralph at its simplest: a single agent loops against a PROMPT.md until it outputs LOOP_COMPLETE or hits the iteration limit. No hats, no events — just a loop.
|
||||||
|
|
||||||
|
This is the right choice for most tasks. Don't reach for hats unless you genuinely need distinct phases with different mindsets.
|
||||||
|
|
||||||
|
## ralph.yml Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cli:
|
||||||
|
backend: "claude" # or: kiro, gemini, codex, amp, copilot, opencode
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
max_iterations: 50 # Start conservative, increase if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Options
|
||||||
|
|
||||||
|
| Backend | CLI Tool | Notes |
|
||||||
|
|---------|----------|-------|
|
||||||
|
| claude | Claude Code | Recommended. Best reasoning, large context window |
|
||||||
|
| kiro | Kiro | AWS-integrated |
|
||||||
|
| gemini | Gemini CLI | Cost-effective |
|
||||||
|
| codex | Codex | OpenAI agent |
|
||||||
|
| amp | Amp | Sourcegraph agent |
|
||||||
|
| copilot | Copilot CLI | GitHub integrated |
|
||||||
|
| opencode | OpenCode | Open source |
|
||||||
|
|
||||||
|
## PROMPT.md Examples
|
||||||
|
|
||||||
|
### Example 1: Build a Feature
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Task: Add User Authentication to Express API
|
||||||
|
|
||||||
|
Add JWT-based authentication to the existing Express.js API.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- POST /auth/login accepts email + password, returns JWT
|
||||||
|
- POST /auth/register creates a new user account
|
||||||
|
- Middleware protects all /users/* routes
|
||||||
|
- Tokens expire after 24 hours
|
||||||
|
- Passwords are hashed with bcrypt
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
All of the following must be true:
|
||||||
|
- [ ] POST /auth/register creates a user and returns 201
|
||||||
|
- [ ] POST /auth/login returns a valid JWT for correct credentials
|
||||||
|
- [ ] POST /auth/login returns 401 for incorrect credentials
|
||||||
|
- [ ] Protected routes return 401 without a valid token
|
||||||
|
- [ ] Protected routes work normally with a valid token
|
||||||
|
- [ ] All existing tests still pass
|
||||||
|
- [ ] New tests cover all auth endpoints
|
||||||
|
- [ ] TypeScript compiles with zero errors
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Use jsonwebtoken for JWT handling
|
||||||
|
- Use bcrypt for password hashing
|
||||||
|
- Follow existing code patterns in src/
|
||||||
|
- Do not modify existing endpoint behaviour
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Fix a Bug
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Task: Fix Race Condition in WebSocket Handler
|
||||||
|
|
||||||
|
The WebSocket message handler has a race condition where concurrent connections
|
||||||
|
can corrupt shared state. Messages are being delivered to wrong clients.
|
||||||
|
|
||||||
|
## Current Behaviour
|
||||||
|
|
||||||
|
When 2+ clients send messages simultaneously, responses sometimes go to the
|
||||||
|
wrong client. See issue #247 for reproduction steps.
|
||||||
|
|
||||||
|
## Expected Behaviour
|
||||||
|
|
||||||
|
Each client receives only their own responses, regardless of concurrency.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Concurrent WebSocket test passes (test/ws-concurrent.test.ts)
|
||||||
|
- [ ] Existing WebSocket tests still pass
|
||||||
|
- [ ] No shared mutable state between connection handlers
|
||||||
|
- [ ] Load test with 50 concurrent connections shows zero cross-talk
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do not change the public WebSocket API
|
||||||
|
- Fix must work with the existing Redis pub/sub setup
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Write a Script
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Task: CSV Data Migration Script
|
||||||
|
|
||||||
|
Create a Python script that migrates data from the legacy CSV format to the
|
||||||
|
new database schema.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Read CSV files from data/legacy/*.csv
|
||||||
|
- Transform fields according to the mapping in docs/migration-map.md
|
||||||
|
- Insert into PostgreSQL using the existing SQLAlchemy models
|
||||||
|
- Handle duplicates by updating existing records
|
||||||
|
- Log all skipped/failed rows to migration_errors.log
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Script processes all CSV files in data/legacy/
|
||||||
|
- [ ] All valid rows are inserted or updated in the database
|
||||||
|
- [ ] Duplicate handling works correctly (update, don't duplicate)
|
||||||
|
- [ ] Error log captures all skipped rows with reasons
|
||||||
|
- [ ] Script completes without unhandled exceptions
|
||||||
|
- [ ] Unit tests cover the transformation logic
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- Use existing SQLAlchemy models from src/models/
|
||||||
|
- Must be idempotent (safe to run multiple times)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic run
|
||||||
|
ralph run
|
||||||
|
|
||||||
|
# With iteration limit
|
||||||
|
ralph run --max-iterations 30
|
||||||
|
|
||||||
|
# Resume an interrupted session
|
||||||
|
ralph run --continue
|
||||||
|
|
||||||
|
# Quiet mode (no TUI)
|
||||||
|
ralph run -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Upgrade to Hats
|
||||||
|
|
||||||
|
If you find the simple prompt struggling because:
|
||||||
|
- The agent keeps flip-flopping between planning and coding
|
||||||
|
- It loses track of the overall architecture while implementing details
|
||||||
|
- It writes code but never stops to review/test properly
|
||||||
|
- The task is too large for a single coherent prompt
|
||||||
|
|
||||||
|
...then consider switching to hat-based mode. But try simplifying the prompt first — often the issue is a vague prompt, not a need for hats.
|
||||||
+13
-7
@@ -1,11 +1,11 @@
|
|||||||
# Session Handoff
|
# Session Handoff
|
||||||
|
|
||||||
_Generated: 2026-02-16 10:43:45 UTC_
|
_Generated: 2026-02-16 11:04:21 UTC_
|
||||||
|
|
||||||
## Git Context
|
## Git Context
|
||||||
|
|
||||||
- **Branch:** `codex/kpi`
|
- **Branch:** `codex/projects`
|
||||||
- **HEAD:** 24ffe03: chore: auto-commit before merge (loop primary)
|
- **HEAD:** 78e994e: chore: auto-commit before merge (loop primary)
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -13,22 +13,28 @@ _Generated: 2026-02-16 10:43:45 UTC_
|
|||||||
|
|
||||||
- [x] Compact Latest Results KPI section
|
- [x] Compact Latest Results KPI section
|
||||||
- [x] Validate KPI objective and close loop
|
- [x] Validate KPI objective and close loop
|
||||||
|
- [x] Rename Active Projects language to Significant Interventions
|
||||||
|
- [x] Add autoplay + reduced-motion behavior for carousel
|
||||||
|
- [x] Responsive polish and full verification for interventions carousel
|
||||||
|
- [x] Implement Embla carousel in ProjectsTile
|
||||||
|
- [x] Add autoplay + reduced-motion behavior for carousel
|
||||||
|
- [x] Responsive polish and full verification for interventions carousel
|
||||||
|
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
Recently modified:
|
Recently modified:
|
||||||
|
|
||||||
- `.ralph/agent/handoff.md`
|
|
||||||
- `.ralph/agent/memories.md`
|
- `.ralph/agent/memories.md`
|
||||||
- `.ralph/agent/memories.md.lock`
|
|
||||||
- `.ralph/agent/scratchpad.md`
|
- `.ralph/agent/scratchpad.md`
|
||||||
- `.ralph/agent/summary.md`
|
- `.ralph/agent/summary.md`
|
||||||
- `.ralph/agent/tasks.jsonl`
|
- `.ralph/agent/tasks.jsonl`
|
||||||
- `.ralph/agent/tasks.jsonl.lock`
|
|
||||||
- `.ralph/current-events`
|
- `.ralph/current-events`
|
||||||
- `.ralph/current-loop-id`
|
- `.ralph/current-loop-id`
|
||||||
- `.ralph/events-20260216-103430.jsonl`
|
- `.ralph/events-20260216-105626.jsonl`
|
||||||
|
- `.ralph/history.jsonl`
|
||||||
|
- `.ralph/loop.lock`
|
||||||
|
- `package-lock.json`
|
||||||
|
|
||||||
## Next Session
|
## Next Session
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
|
### mem-1771239841-81ef
|
||||||
|
> ProjectsTile responsive layout now uses cards-per-view width calc plus flex gap instead of slide padding to prevent overflow/cropping across breakpoints.
|
||||||
|
<!-- tags: ui, carousel, responsive | created: 2026-02-16 -->
|
||||||
|
|
||||||
|
### mem-1771239746-fb8e
|
||||||
|
> Embla autoplay in ProjectsTile uses playOnInit=false with explicit play/stop tied to prefers-reduced-motion to avoid first-render motion flicker.
|
||||||
|
<!-- tags: ui, carousel, accessibility | created: 2026-02-16 -->
|
||||||
|
|
||||||
|
### mem-1771239639-a457
|
||||||
|
> ProjectsTile now uses Embla carousel with autoplay disabled under prefers-reduced-motion and preserves project detail panel activation via click/keyboard.
|
||||||
|
<!-- tags: ui, carousel, accessibility | created: 2026-02-16 -->
|
||||||
|
|
||||||
|
### mem-1771239522-007a
|
||||||
|
> Projects terminology baseline updated: dashboard tile, subnav label, and search palette section now use 'Significant Interventions' instead of 'Active Projects'/'Projects'.
|
||||||
|
<!-- tags: ui, search, naming | created: 2026-02-16 -->
|
||||||
|
|
||||||
### mem-1771238197-12d0
|
### mem-1771238197-12d0
|
||||||
> Latest Results KPI tile now uses a dedicated responsive grid class: mobile defaults to 1 column and md+ forces 4 columns; coachmark/pulse behavior removed from PatientSummaryTile and related CSS.
|
> Latest Results KPI tile now uses a dedicated responsive grid class: mobile defaults to 1 column and md+ forces 4 columns; coachmark/pulse behavior removed from PatientSummaryTile and related CSS.
|
||||||
<!-- tags: ui, layout, kpi | created: 2026-02-16 -->
|
<!-- tags: ui, layout, kpi | created: 2026-02-16 -->
|
||||||
@@ -10,6 +26,10 @@
|
|||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
|
### mem-1771239420-0b3f
|
||||||
|
> failure: cmd=sed -n '1,220p' Ralph/PROMPT.md and sed -n '1,220p' .ralph/agent/scratchpad.md, exit=2, error=path mismatch (Ralph/prompt.md is lowercase) and missing scratchpad file, next=use correct lowercase prompt path and recreate scratchpad before proceeding
|
||||||
|
<!-- tags: tooling, error-handling, ralph | created: 2026-02-16 -->
|
||||||
|
|
||||||
### mem-1771238608-ecff
|
### mem-1771238608-ecff
|
||||||
> failure: cmd=git commit -m 'chore: document KPI objective verification', exit=128, error=.git/index.lock exists due concurrent git operations, next=run git commands sequentially and remove stale lock after confirming no active git process
|
> failure: cmd=git commit -m 'chore: document KPI objective verification', exit=128, error=.git/index.lock exists due concurrent git operations, next=run git commands sequentially and remove stale lock after confirming no active git process
|
||||||
<!-- tags: tooling, error-handling, git | created: 2026-02-16 -->
|
<!-- tags: tooling, error-handling, git | created: 2026-02-16 -->
|
||||||
|
|||||||
+97
-12
@@ -1,16 +1,101 @@
|
|||||||
## 2026-02-16T10:43:30Z
|
# Scratchpad
|
||||||
Started new loop iteration for `Ralph/PROMPT.md` objective (Latest Results KPI compaction). Reviewed objective, handoff, summary, and current implementation.
|
|
||||||
|
|
||||||
Observation: implementation in `src/components/tiles/PatientSummaryTile.tsx` and `src/index.css` already appears to satisfy the requested changes (coachmark removed, helper text moved into header row, responsive 1-column mobile and 4-column md+ grid, compact metric card spacing).
|
## 2026-02-16T10:57:00Z
|
||||||
|
Objective loaded from `Ralph/prompt.md`: replace the current Active Projects list with a Significant Interventions Embla carousel with autoplay/reduced-motion handling, preserve panel-open behavior, and update related labels.
|
||||||
|
|
||||||
Plan for this iteration:
|
Current baseline:
|
||||||
1) Create one runtime task to validate objective state and close loop.
|
- `src/components/tiles/ProjectsTile.tsx` is still a static vertical list with heading `ACTIVE PROJECTS`.
|
||||||
2) Run required verification commands (`npm run typecheck`, `npm run lint`, `npm run build`).
|
- `src/components/SubNav.tsx` still labels the section `Projects`.
|
||||||
3) If checks pass, close task and emit `LOOP_COMPLETE`.
|
- `src/lib/search.ts` still uses palette section label `Active Projects` and section order references.
|
||||||
|
- Embla dependencies are not present in `package.json`.
|
||||||
|
|
||||||
Verification run completed in this iteration:
|
Plan for looped atomic execution:
|
||||||
- `npm run typecheck`: pass
|
1. Create runtime tasks covering naming updates, carousel implementation, and validation.
|
||||||
- `npm run lint`: pass (2 existing warnings in context files only, no errors)
|
2. Execute one atomic task this iteration: rename user-facing labels from Active Projects/Projects to Significant Interventions in the prompt-specified files (`ProjectsTile`, `SubNav`, `search`).
|
||||||
- `npm run build`: pass (existing bundle-size warning only)
|
3. Verify with targeted checks (`npm run typecheck` at minimum for this text/code change), commit atomically, close task, and emit completion event for this iteration.
|
||||||
|
|
||||||
Decision: treat objective as complete because acceptance criteria are already present in source and validation is green.
|
Confidence in this decomposition: 92/100 (high). Proceeding autonomously.
|
||||||
|
|
||||||
|
## 2026-02-16T10:59:30Z
|
||||||
|
Completed task `task-1771239437-05c3`.
|
||||||
|
|
||||||
|
Implemented changes:
|
||||||
|
- `src/components/tiles/ProjectsTile.tsx`: tile header updated to `SIGNIFICANT INTERVENTIONS`.
|
||||||
|
- `src/components/SubNav.tsx`: section label updated to `Significant Interventions`.
|
||||||
|
- `src/lib/search.ts`: palette section type/value/order/comments updated from `Active Projects` to `Significant Interventions`.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `npm run lint` passed with existing baseline warnings only (no errors).
|
||||||
|
- `npm run typecheck` passed.
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
- `98d767f` — `feat: rename Active Projects references to Significant Interventions`.
|
||||||
|
|
||||||
|
Remaining open tasks are implementation-focused (`ProjectsTile` Embla carousel + autoplay/responsive polish).
|
||||||
|
|
||||||
|
## 2026-02-16T11:00:29Z
|
||||||
|
Picked ready task `task-1771239437-6f55` (P1): implement Embla carousel in `ProjectsTile` while preserving panel-open behavior.
|
||||||
|
|
||||||
|
Implementation completed in one atomic pass:
|
||||||
|
- Installed dependencies: `embla-carousel-react`, `embla-carousel-autoplay`.
|
||||||
|
- Refactored `src/components/tiles/ProjectsTile.tsx` from static vertical list to Embla viewport/container/slide structure.
|
||||||
|
- Added per-slide thumbnail placeholder region (asset-ready later), preserved click + Enter/Space activation path to `openPanel({ type: 'project', investigation: project })`.
|
||||||
|
- Added autoplay plugin defaults (delay, pause on hover/focus, resume after interaction) and reduced-motion detection via `matchMedia('(prefers-reduced-motion: reduce)')` to disable autoplay.
|
||||||
|
- Added responsive slide sizing logic: mobile 1-up, tablet 2-up, desktop 3-up.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `npm run lint` passed with pre-existing warnings only.
|
||||||
|
- `npm run typecheck` passed.
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
Confidence: 93/100. Ready to commit and close task.
|
||||||
|
|
||||||
|
## 2026-02-16T11:01:20Z
|
||||||
|
Task `task-1771239437-6f55` completed and committed as `5fa01b8` (`feat: implement Embla carousel in ProjectsTile`).
|
||||||
|
|
||||||
|
Post-close ready queue now exposes follow-up tasks:
|
||||||
|
- `task-1771239444-811f` (autoplay + reduced-motion behavior tuning)
|
||||||
|
- `task-1771239444-850d` (responsive polish + full verification)
|
||||||
|
|
||||||
|
Stopping after one atomic task per loop contract.
|
||||||
|
|
||||||
|
## 2026-02-16T11:02:18Z
|
||||||
|
Picked ready task `task-1771239444-811f` (P2): autoplay + reduced-motion behavior tuning for the interventions Embla carousel.
|
||||||
|
|
||||||
|
Adjustment made in `src/components/tiles/ProjectsTile.tsx`:
|
||||||
|
- Switched autoplay plugin creation to a stable `useRef` instance.
|
||||||
|
- Set `playOnInit: false` to prevent motion before user preference is known.
|
||||||
|
- Initialized `prefersReducedMotion` from `matchMedia('(prefers-reduced-motion: reduce)')` during first render.
|
||||||
|
- Added effect that explicitly `play()`/`stop()`s autoplay based on current reduced-motion preference once Embla API is available.
|
||||||
|
|
||||||
|
Result:
|
||||||
|
- Carousel autoplay remains enabled by default for motion-allowed users.
|
||||||
|
- Autoplay is disabled reliably for reduced-motion users, including initial render path.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `npm run lint` passed (existing baseline warnings only in context providers).
|
||||||
|
- `npm run typecheck` passed.
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
Confidence in task completion: 94/100.
|
||||||
|
|
||||||
|
## 2026-02-16T11:08:10Z
|
||||||
|
Picked ready task `task-1771239444-850d` (P2): responsive polish and full verification for interventions carousel.
|
||||||
|
|
||||||
|
Responsive polish implemented in `src/components/tiles/ProjectsTile.tsx`:
|
||||||
|
- Replaced per-slide right-padding + container negative margin spacing with flex `gap: 12px` to avoid width bleed/cropping at narrow widths.
|
||||||
|
- Changed slide sizing from raw percent basis to computed width formula using cards-per-view breakpoints:
|
||||||
|
- mobile: 1 card
|
||||||
|
- tablet: 2 cards
|
||||||
|
- desktop: 3 cards
|
||||||
|
Width expression: `calc((100% - totalGap) / cardsPerView)`.
|
||||||
|
- Added focus/blur card styling parity with hover styling for keyboard users.
|
||||||
|
- Tuned Embla options for smoother snap behavior across breakpoints (`dragFree: false`, `slidesToScroll: 1`).
|
||||||
|
|
||||||
|
Verification (full):
|
||||||
|
- `npm run lint` passed (existing baseline warnings only in context provider files).
|
||||||
|
- `npm run typecheck` passed.
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
Confidence in completion: 95/100.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Loop Summary
|
# Loop Summary
|
||||||
|
|
||||||
**Status:** Completed successfully
|
**Status:** Completed successfully
|
||||||
**Iterations:** 1
|
**Iterations:** 4
|
||||||
**Duration:** 1m 35s
|
**Duration:** 7m 17s
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -14,4 +14,4 @@ _No events recorded._
|
|||||||
|
|
||||||
## Final Commit
|
## Final Commit
|
||||||
|
|
||||||
e5c7d9b: chore: document KPI objective verification
|
68f92fb: feat: polish interventions carousel responsiveness
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
{"id":"task-1771238094-7dc9","title":"Compact Latest Results KPI section","description":"Remove coachmark/pulse, move instruction text to heading row right area, enforce 1x4 mobile and 4x1 md+ KPI layout, reduce KPI card whitespace in PatientSummaryTile while preserving content/interactions.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-103430","created":"2026-02-16T10:34:54.490955020+00:00","closed":"2026-02-16T10:36:37.836478822+00:00"}
|
{"id":"task-1771238094-7dc9","title":"Compact Latest Results KPI section","description":"Remove coachmark/pulse, move instruction text to heading row right area, enforce 1x4 mobile and 4x1 md+ KPI layout, reduce KPI card whitespace in PatientSummaryTile while preserving content/interactions.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-103430","created":"2026-02-16T10:34:54.490955020+00:00","closed":"2026-02-16T10:36:37.836478822+00:00"}
|
||||||
{"id":"task-1771238560-5ec5","title":"Validate KPI objective and close loop","description":"Run typecheck/lint/build and confirm Latest Results KPI compaction acceptance criteria remain satisfied before LOOP_COMPLETE event.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-104201","created":"2026-02-16T10:42:40.351948381+00:00","closed":"2026-02-16T10:43:32.976626807+00:00"}
|
{"id":"task-1771238560-5ec5","title":"Validate KPI objective and close loop","description":"Run typecheck/lint/build and confirm Latest Results KPI compaction acceptance criteria remain satisfied before LOOP_COMPLETE event.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-104201","created":"2026-02-16T10:42:40.351948381+00:00","closed":"2026-02-16T10:43:32.976626807+00:00"}
|
||||||
|
{"id":"task-1771239437-05c3","title":"Rename Active Projects language to Significant Interventions","description":"Update Projects tile heading, SubNav label, and search palette section naming per Ralph prompt.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.787908637+00:00","closed":"2026-02-16T10:58:29.985946409+00:00"}
|
||||||
|
{"id":"task-1771239437-64c3","title":"Add autoplay + reduced-motion behavior for carousel","description":"Install Embla autoplay plugin, pause on interaction, disable autoplay for prefers-reduced-motion users.","status":"closed","priority":2,"blocked_by":["task-placeholder"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.812228675+00:00","closed":"2026-02-16T10:57:24.084148333+00:00"}
|
||||||
|
{"id":"task-1771239437-67bc","title":"Responsive polish and full verification for interventions carousel","description":"Tune mobile/desktop viewport behavior and run lint/typecheck/build before closure.","status":"closed","priority":2,"blocked_by":["task-placeholder"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.812991662+00:00","closed":"2026-02-16T10:57:24.085921620+00:00"}
|
||||||
|
{"id":"task-1771239437-6f55","title":"Implement Embla carousel in ProjectsTile","description":"Replace list layout with Embla carousel slides, preserve click/keyboard panel opening, add thumbnail placeholder region.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.814939655+00:00","closed":"2026-02-16T11:00:49.359576691+00:00"}
|
||||||
|
{"id":"task-1771239444-811f","title":"Add autoplay + reduced-motion behavior for carousel","description":"Install Embla autoplay plugin, pause on interaction, disable autoplay for prefers-reduced-motion users.","status":"closed","priority":2,"blocked_by":["task-1771239437-6f55"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:24.098597492+00:00","closed":"2026-02-16T11:02:34.691389297+00:00"}
|
||||||
|
{"id":"task-1771239444-850d","title":"Responsive polish and full verification for interventions carousel","description":"Tune mobile/desktop viewport behavior and run lint/typecheck/build before closure.","status":"closed","priority":2,"blocked_by":["task-1771239437-6f55"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:24.099597797+00:00","closed":"2026-02-16T11:04:10.599296057+00:00"}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
.ralph/events-20260216-104201.jsonl
|
.ralph/events-20260216-105626.jsonl
|
||||||
@@ -1 +1 @@
|
|||||||
primary-20260216-104201
|
primary-20260216-105626
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{"ts":"2026-02-16T10:56:26.167394244+00:00","iteration":0,"hat":"loop","topic":"task.start","triggered":"planner","payload":"Ralph/PROMPT.md"}
|
||||||
|
{"payload":"completed: rename Significant Interventions labels, verify lint/typecheck/build, commit 98d767f; next ready task: task-1771239437-6f55","topic":"task.done","ts":"2026-02-16T10:58:44.650889107+00:00"}
|
||||||
|
{"payload":"completed: task-1771239437-6f55, commit: 5fa01b8, checks: lint/typecheck/build pass, next ready: task-1771239444-811f","topic":"task.done","ts":"2026-02-16T11:00:56.293728172+00:00"}
|
||||||
|
{"payload":"completed: task-1771239444-811f, commit: be7a65e, checks: lint/typecheck/build pass, next ready: task-1771239444-850d","topic":"task.done","ts":"2026-02-16T11:02:38.562433372+00:00"}
|
||||||
|
{"payload":"objective complete: Significant Interventions carousel implemented with autoplay/reduced-motion, responsive polish, and lint/typecheck/build passing","topic":"LOOP_COMPLETE","ts":"2026-02-16T11:04:17.800172602+00:00"}
|
||||||
|
{"ts":"2026-02-16T11:04:21.786823515+00:00","iteration":4,"hat":"loop","topic":"loop.terminate","payload":"## Reason\ncompleted\n\n## Status\nAll tasks completed successfully.\n\n## Summary\n- Iterations: 4\n- Duration: 7m 17s\n- Exit code: 0"}
|
||||||
@@ -2,3 +2,5 @@
|
|||||||
{"ts":"2026-02-16T10:36:47.670503849Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
|
{"ts":"2026-02-16T10:36:47.670503849Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
|
||||||
{"ts":"2026-02-16T10:42:01.215892851Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}}
|
{"ts":"2026-02-16T10:42:01.215892851Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}}
|
||||||
{"ts":"2026-02-16T10:43:44.925586089Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
|
{"ts":"2026-02-16T10:43:44.925586089Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
|
||||||
|
{"ts":"2026-02-16T10:56:26.267912429Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}}
|
||||||
|
{"ts":"2026-02-16T11:04:21.788867135Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
|
||||||
|
|||||||
+2
-8
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
<<<<<<< HEAD
|
"pid": 892085,
|
||||||
"pid": 864891,
|
"started": "2026-02-16T10:56:26.145878153Z",
|
||||||
"started": "2026-02-16T10:14:58.914587907Z",
|
|
||||||
"prompt": "[no prompt]"
|
|
||||||
=======
|
|
||||||
"pid": 883596,
|
|
||||||
"started": "2026-02-16T10:42:01.108766214Z",
|
|
||||||
"prompt": "Ralph/PROMPT.md"
|
"prompt": "Ralph/PROMPT.md"
|
||||||
>>>>>>> codex/kpi
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Task: Sidebar-First Navigation Refactor (Remove Top Navbar/Subnav)
|
||||||
|
|
||||||
|
Refactor the dashboard so navigation is fully sidebar-driven, with clear recruiter-facing labels and robust responsive behavior. The current layout is still tied to an older navbar/subnav model and shows incorrect scroll behavior in the sidebar area.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Current implementation has separate top navigation (`TopBar`, `SubNav`) and a desktop-only sidebar. On upward scrolling in the sidebar, hidden space becomes visible in a way that implies layered layout offsets from the old top navbar/subnav structure.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Remove top navbar/subnav from the rendered dashboard flow and migrate section navigation into the sidebar.
|
||||||
|
- Replace section labels with recruiter-facing content labels (no GP/internal metaphors as labels):
|
||||||
|
- Overview
|
||||||
|
- Projects
|
||||||
|
- Experience
|
||||||
|
- Education
|
||||||
|
- Skills
|
||||||
|
- Keep iconography that can still evoke the GP-system metaphor, but labels must match actual portfolio content.
|
||||||
|
- Add a `Navigation` subheader area in the sidebar for section links.
|
||||||
|
- Keep a separate `My Data` area above `Navigation` in expanded sidebar mode.
|
||||||
|
- Ensure the sidebar no longer reveals hidden spacing/artifacts when scrolling upward.
|
||||||
|
- Implement mobile sidebar behavior (currently missing):
|
||||||
|
- Sidebar is collapsed by default.
|
||||||
|
- A hamburger control appears at the top and toggles expanded/collapsed state.
|
||||||
|
- In collapsed mode, render a compact vertical rail with:
|
||||||
|
- hamburger control at the top
|
||||||
|
- the five section icons directly beneath for one-tap section jumping
|
||||||
|
- In expanded mode, reveal full sidebar content:
|
||||||
|
- `My Data` block
|
||||||
|
- `Navigation` links with icon + text labels
|
||||||
|
- tags, alerts, and highlights sections
|
||||||
|
- Preserve or improve accessibility:
|
||||||
|
- Keyboard operable controls
|
||||||
|
- Correct `aria-*` labels for menu toggle and navigation regions
|
||||||
|
- Visible focus states
|
||||||
|
- Preserve smooth section scrolling/anchor behavior from navigation actions.
|
||||||
|
|
||||||
|
## Suggested GP-Metaphor Icon Mapping (labels remain recruiter-facing)
|
||||||
|
|
||||||
|
Use these concrete icon targets (or closest equivalents from existing icon library):
|
||||||
|
|
||||||
|
- Overview: `UserRound` (profile summary)
|
||||||
|
- Projects: `Pill` (interventions/medications metaphor)
|
||||||
|
- Experience: `Workflow` (pathway/Sankey metaphor)
|
||||||
|
- Education: `GraduationCap` (training/education)
|
||||||
|
- Skills: `Wrench` (capabilities/tools)
|
||||||
|
|
||||||
|
Label text must stay recruiter-facing:
|
||||||
|
- `Overview`, `Projects`, `Experience`, `Education`, `Skills`
|
||||||
|
|
||||||
|
## Likely Files In Scope
|
||||||
|
|
||||||
|
- `src/components/DashboardLayout.tsx`
|
||||||
|
- `src/components/Sidebar.tsx`
|
||||||
|
- `src/components/SubNav.tsx`
|
||||||
|
- `src/components/TopBar.tsx`
|
||||||
|
- `src/index.css`
|
||||||
|
- Any related hooks/types/styles needed for section activity and responsive state
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
All of the following must be true:
|
||||||
|
|
||||||
|
- [ ] No top navbar/subnav is rendered in the final dashboard layout.
|
||||||
|
- [ ] Sidebar contains the five required recruiter-facing nav labels under a `Navigation` subheader.
|
||||||
|
- [ ] Expanded sidebar includes a distinct `My Data` area above `Navigation`.
|
||||||
|
- [ ] Sidebar scrolling no longer exposes hidden top spacing/artifacts when scrolling upward.
|
||||||
|
- [ ] Desktop navigation from sidebar correctly jumps/scrolls to each section.
|
||||||
|
- [ ] On mobile, sidebar is collapsed by default with hamburger at top and five icon shortcuts visible.
|
||||||
|
- [ ] On mobile expand, sidebar shows `My Data`, full navigation links (icon + text), and tags/alerts/highlights.
|
||||||
|
- [ ] Navigation controls are keyboard accessible with appropriate ARIA semantics.
|
||||||
|
- [ ] `npm run lint` passes.
|
||||||
|
- [ ] `npm run typecheck` passes.
|
||||||
|
- [ ] `npm run build` passes.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Use the existing project stack and conventions (TypeScript + React + current design language).
|
||||||
|
- Do not reintroduce GP-style labels like "Significant Interventions" or "Patient Summary" for the sidebar nav text.
|
||||||
|
- Keep changes focused on layout/navigation behavior; avoid unrelated refactors.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Track implementation progress in this file or `.ralph/plan.md`.
|
||||||
|
When all success criteria are met, print LOOP_COMPLETE.
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Significant Interventions Carousel (Ralph Prompt)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Replace the current one-column **Active Projects** list with a **Significant Interventions** carousel that supports thumbnail cards and auto-scroll behavior (Embla-based), while preserving panel-open behavior on card click.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
- Rename all relevant UI/content references from **Active Projects** to **Significant Interventions**.
|
|
||||||
- Replace `ProjectsTile` list layout with an Embla carousel.
|
|
||||||
- Use auto-scroll as the default carousel behavior.
|
|
||||||
- Keep room for thumbnails now; real thumbnail assets will be added later.
|
|
||||||
|
|
||||||
## Implementation Task List
|
|
||||||
|
|
||||||
- [ ] Install carousel dependencies:
|
|
||||||
- `embla-carousel-react`
|
|
||||||
- `embla-carousel-autoplay`
|
|
||||||
- [ ] Update tile heading in `src/components/tiles/ProjectsTile.tsx`:
|
|
||||||
- `ACTIVE PROJECTS` -> `SIGNIFICANT INTERVENTIONS`
|
|
||||||
- [ ] Refactor `ProjectsTile` in `src/components/tiles/ProjectsTile.tsx`:
|
|
||||||
- Replace vertical list container with Embla viewport/container/slides
|
|
||||||
- Convert each project item to a carousel slide card
|
|
||||||
- Add thumbnail region in each slide (use placeholder block/image container for now)
|
|
||||||
- Keep keyboard activation (`Enter`/`Space`) and click-to-open detail panel
|
|
||||||
- [ ] Implement auto-scroll behavior:
|
|
||||||
- Use Embla autoplay plugin with sensible defaults (continuous feel, pauses on hover/focus)
|
|
||||||
- Respect reduced motion (`prefers-reduced-motion`) by disabling autoplay
|
|
||||||
- [ ] Responsive behavior:
|
|
||||||
- Mobile: single-card view
|
|
||||||
- Tablet/Desktop: multi-card visible area (based on available width)
|
|
||||||
- Ensure overflow clipping and smooth transitions
|
|
||||||
- [ ] Update navigation/search labels to match naming:
|
|
||||||
- `src/components/SubNav.tsx`: `Projects` -> `Significant Interventions`
|
|
||||||
- `src/lib/search.ts`: `Active Projects` -> `Significant Interventions` (section type and related labels/comments)
|
|
||||||
- [ ] Keep detail panel integration unchanged:
|
|
||||||
- Clicking a carousel card still calls `openPanel({ type: 'project', investigation: project })`
|
|
||||||
- [ ] Styling pass:
|
|
||||||
- Align with current dashboard tokens (`--surface`, `--border-light`, `--accent`, etc.)
|
|
||||||
- Ensure cards remain readable without thumbnails
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- The dashboard section title displays **Significant Interventions**.
|
|
||||||
- The old one-column projects list is replaced by a working carousel.
|
|
||||||
- Carousel auto-scrolls by default and pauses appropriately on interaction.
|
|
||||||
- In reduced-motion environments, carousel does not auto-scroll.
|
|
||||||
- Clicking or keyboard-activating a card opens the existing project detail panel.
|
|
||||||
- Layout works on mobile and desktop without overflow bugs.
|
|
||||||
- Search/navigation language no longer references **Active Projects**.
|
|
||||||
|
|
||||||
## Notes for Implementation
|
|
||||||
|
|
||||||
- Thumbnail assets are intentionally deferred; implement with placeholders now.
|
|
||||||
- Keep the component name `ProjectsTile` for this pass to minimize refactor risk; rename component/file in a later cleanup task if desired.
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Existing Tokens to Replace/Update
|
|
||||||
|
|
||||||
The Tailwind config and CSS already have tokens from the old PMR design. Task 1 needs to UPDATE these, not just add new ones alongside:
|
|
||||||
|
|
||||||
**Existing Tailwind shadow tokens (replace with new three-tier system):**
|
|
||||||
- `pmr`: `'0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03)'` → replace with `pmr-sm`
|
|
||||||
- `pmr-hover`: `'0 2px 4px rgba(0,0,0,0.06), 0 8px 16px rgba(0,0,0,0.04)'` → replace with `pmr-md`
|
|
||||||
- `pmr-banner`: `'0 2px 8px rgba(0,0,0,0.12)'` → remove (no banner in new design)
|
|
||||||
|
|
||||||
**Existing Tailwind color tokens (keep during transition, Task 21 cleans up):**
|
|
||||||
- `pmr-nhsblue: '#005EB8'` — keep for login screen (still uses NHS blue)
|
|
||||||
- `pmr-content: '#F5F7FA'` → update to `pmr-content: '#F0F5F4'` (new bg color)
|
|
||||||
- `pmr-sidebar: '#1E293B'` → update to `pmr-sidebar: '#F7FAFA'` (light sidebar)
|
|
||||||
|
|
||||||
**Existing CSS custom properties (in `--pmr-*` namespace):**
|
|
||||||
- Previous iterations added `--pmr-*` variables. The new tokens use shorter names (e.g., `--bg`, `--surface`, `--accent`). Add the new tokens AND keep `--pmr-*` aliases during transition so existing components don't break before they're rebuilt.
|
|
||||||
|
|
||||||
**Existing border-radius tokens:**
|
|
||||||
- `card: '4px'` → update to `card: '8px'`
|
|
||||||
- `login: '12px'` — keep unchanged
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# 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`
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# 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`
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Established Patterns (from previous iterations)
|
|
||||||
|
|
||||||
These patterns were established across 16 iterations of the old PMR build. Reuse them:
|
|
||||||
|
|
||||||
### Phase name is `'pmr'`
|
|
||||||
The Phase type in `src/types/index.ts` is `'boot' | 'ecg' | 'login' | 'pmr'`. The `'pmr'` case renders the dashboard. Do NOT rename the phase — just change what it renders.
|
|
||||||
|
|
||||||
### Module-scope `prefersReducedMotion`
|
|
||||||
All animation components should compute this once at module level, not per render:
|
|
||||||
```typescript
|
|
||||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
||||||
```
|
|
||||||
This is the established pattern across all existing view components.
|
|
||||||
|
|
||||||
### Pre-existing ESLint warning
|
|
||||||
`AccessibilityContext.tsx` has 1 pre-existing ESLint warning. This is expected — do not attempt to fix it. Quality checks pass with this warning present.
|
|
||||||
|
|
||||||
### Callback ref pattern for Framer Motion
|
|
||||||
If you need a ref to a `motion.*` element (e.g., for scroll detection), use `useState` + callback ref instead of `useRef`. Framer Motion elements may not be in the DOM when `useEffect` first runs:
|
|
||||||
```typescript
|
|
||||||
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(null)
|
|
||||||
// On the element: ref={el => { if (el) setScrollContainer(el) }}
|
|
||||||
```
|
|
||||||
This avoids null ref issues with animated mount timing.
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# 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).
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
# 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).
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# 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
|
|
||||||
- **Data source:** `import { medications } from '@/data/medications'` (NOT `skills.ts`). The `medications.ts` file has 18 entries, each with a `prescribingHistory` array of `{ year, description }` entries. Map from `skills.ts` to `medications.ts` by matching skill name to medication name (e.g., "Data Analysis" in skills.ts → find the medication with `name: "Data Analysis"` in medications.ts to get its `prescribingHistory`).
|
|
||||||
- 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 v7.0.0, already installed):
|
|
||||||
|
|
||||||
**Existing code:** `src/lib/search.ts` has `buildSearchIndex()` which creates a Fuse index from consultations, medications, problems, investigations, and documents. It groups results by `sectionLabel` via `groupResultsBySection()`. The `SearchResult` interface has `{ id, title, section: ViewId, sectionLabel, highlight }`.
|
|
||||||
|
|
||||||
**What needs changing:**
|
|
||||||
- The `section: ViewId` field is designed for view-switching navigation (navigating to `#consultations`, `#medications`, etc.). The new dashboard has no views — it's a single scrollable page. Results should either scroll to the relevant tile or expand an item within a tile.
|
|
||||||
- Add `skills.ts` data to the index (currently only `medications.ts` is indexed, not the new 5-skill entries)
|
|
||||||
- Add `kpis.ts` data to the index
|
|
||||||
- Add Quick Actions (Download CV, Send Email, View LinkedIn, View Projects)
|
|
||||||
- Update section labels to match palette grouping: "Experience", "Core Skills", "Active Projects", "Achievements", "Education", "Quick Actions"
|
|
||||||
- Add an `action` field to `SearchResult` so each result knows what to do when selected (scroll to tile, expand item, open link, etc.)
|
|
||||||
|
|
||||||
**Config (keep existing):**
|
|
||||||
- `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.
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
# 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 Assess
|
|
||||||
- `src/hooks/useScrollCondensation.ts` — only used by PatientBanner. If PatientBanner is removed, this can go too.
|
|
||||||
- `src/hooks/useBreakpoint.ts` — may still be useful for responsive tile layouts. Check if any new dashboard component uses it. If not, remove.
|
|
||||||
|
|
||||||
### Context to Simplify
|
|
||||||
- `src/contexts/AccessibilityContext.tsx` — the existing context has `activeView`, `setActiveView`, `expandedItemId`, `setExpandedItem` designed for the old view-switching navigation. With the new single-page dashboard:
|
|
||||||
- `activeView` / `setActiveView` are no longer relevant (no view switching)
|
|
||||||
- `expandedItemId` / `setExpandedItem` may still be useful if tiles report their expanded item for accessibility announcements
|
|
||||||
- Assess whether to simplify the context or remove it entirely and manage expansion state locally in each tile
|
|
||||||
- **Note:** This context has 1 pre-existing ESLint warning — that's expected.
|
|
||||||
|
|
||||||
### 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
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
cli:
|
||||||
|
backend: "codex"
|
||||||
|
|
||||||
|
event_loop:
|
||||||
|
prompt_file: "PROMPT.md"
|
||||||
|
starting_event: "task.start"
|
||||||
|
completion_promise: "LOOP_COMPLETE"
|
||||||
|
max_iterations: 50
|
||||||
|
|
||||||
|
backpressure:
|
||||||
|
gates:
|
||||||
|
- name: "lint"
|
||||||
|
command: "npm run lint"
|
||||||
|
on_fail: "retry"
|
||||||
|
- name: "typecheck"
|
||||||
|
command: "npm run typecheck"
|
||||||
|
on_fail: "retry"
|
||||||
|
- name: "build"
|
||||||
|
command: "npm run build"
|
||||||
|
on_fail: "retry"
|
||||||
|
|
||||||
|
hats:
|
||||||
|
planner:
|
||||||
|
name: "Sidebar Workflow Planner"
|
||||||
|
triggers: ["task.start", "review.changes_requested"]
|
||||||
|
publishes: ["plan.ready"]
|
||||||
|
instructions: |
|
||||||
|
Read PROMPT.md first.
|
||||||
|
|
||||||
|
Your role is planning only:
|
||||||
|
- Analyse current layout/nav implementation in the existing codebase.
|
||||||
|
- Create or update .ralph/plan.md with a concrete implementation plan.
|
||||||
|
- Include file-level changes, risks, and accessibility/responsive checks.
|
||||||
|
- If triggered by review.changes_requested, read .ralph/review.md and adapt the plan.
|
||||||
|
|
||||||
|
Do not write implementation code.
|
||||||
|
Emit plan.ready when the plan is ready.
|
||||||
|
|
||||||
|
builder:
|
||||||
|
name: "Sidebar Workflow Builder"
|
||||||
|
triggers: ["plan.ready"]
|
||||||
|
publishes: ["build.done"]
|
||||||
|
instructions: |
|
||||||
|
Read PROMPT.md and .ralph/plan.md first.
|
||||||
|
|
||||||
|
Implement the planned sidebar-focused layout changes:
|
||||||
|
- Move top navigation responsibilities into the sidebar.
|
||||||
|
- Remove obsolete top navbar/subnav behavior from the rendered layout.
|
||||||
|
- Implement desktop and mobile sidebar behavior requested in PROMPT.md.
|
||||||
|
- Keep section labels aligned to actual recruiter-facing content.
|
||||||
|
- Ensure scroll behavior and anchor navigation are correct.
|
||||||
|
|
||||||
|
Update .ralph/plan.md as work is completed.
|
||||||
|
Emit build.done when implementation is complete.
|
||||||
|
|
||||||
|
reviewer:
|
||||||
|
name: "Sidebar Workflow Reviewer"
|
||||||
|
triggers: ["build.done"]
|
||||||
|
publishes: ["review.approved", "review.changes_requested"]
|
||||||
|
instructions: |
|
||||||
|
Read PROMPT.md (and .ralph/plan.md if needed), then verify final behavior.
|
||||||
|
|
||||||
|
Validate against all success criteria and project conventions:
|
||||||
|
- UX behavior (desktop + mobile)
|
||||||
|
- Navigation semantics and labels
|
||||||
|
- Accessibility and interaction quality
|
||||||
|
- Lint/typecheck/build status
|
||||||
|
|
||||||
|
Write findings to .ralph/review.md.
|
||||||
|
If any criteria fail, emit review.changes_requested with specific actionable feedback.
|
||||||
|
If all criteria pass, print LOOP_COMPLETE.
|
||||||
Generated
+39
@@ -12,6 +12,8 @@
|
|||||||
"@xenova/transformers": "^2.17.2",
|
"@xenova/transformers": "^2.17.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
@@ -3286,6 +3288,43 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/embla-carousel": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-autoplay": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"embla-carousel": "8.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-react": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"embla-carousel": "8.6.0",
|
||||||
|
"embla-carousel-reactive-utils": "8.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-reactive-utils": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"embla-carousel": "8.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
"@xenova/transformers": "^2.17.2",
|
"@xenova/transformers": "^2.17.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
|
|||||||
@@ -1,34 +1,7 @@
|
|||||||
# Ralph Orchestrator Configuration
|
|
||||||
# Generated by: ralph init --backend codex
|
|
||||||
# Docs: https://github.com/mikeyobrien/ralph-orchestrator
|
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
backend: "codex"
|
backend: "codex"
|
||||||
|
|
||||||
event_loop:
|
event_loop:
|
||||||
prompt_file: "PROMPT.md"
|
prompt_file: "PROMPT.md"
|
||||||
completion_promise: "LOOP_COMPLETE"
|
completion_promise: "LOOP_COMPLETE"
|
||||||
max_iterations: 100
|
max_iterations: 50
|
||||||
# max_runtime_seconds: 14400 # 4 hours max
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
# Additional Configuration (uncomment to customize)
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# core:
|
|
||||||
# scratchpad: ".ralph/agent/scratchpad.md"
|
|
||||||
# specs_dir: ".ralph/specs/"
|
|
||||||
|
|
||||||
# Custom hats for multi-agent workflows:
|
|
||||||
# hats:
|
|
||||||
# builder:
|
|
||||||
# name: "Builder"
|
|
||||||
# triggers: ["build.task"]
|
|
||||||
# publishes: ["build.done", "build.blocked"]
|
|
||||||
#
|
|
||||||
# reviewer:
|
|
||||||
# name: "Reviewer"
|
|
||||||
# triggers: ["review.request"]
|
|
||||||
# publishes: ["review.approved", "review.changes_requested"]
|
|
||||||
|
|
||||||
# Create PROMPT.md with your task, then run: ralph run
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
# Ralph Progress Log
|
# Ralph Progress Log
|
||||||
Started: Sat Feb 14 02:26:59 GMT 2026
|
Started: Sat Feb 14 02:26:59 GMT 2026
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Manual Intervention -- 2026-02-16
|
||||||
|
### Reason: Sidebar navigation architecture needs a structural reset to remove navbar artifacts and align labels with recruiter-facing content.
|
||||||
|
### Changes made:
|
||||||
|
- Added `Ralph/prompts.md` with a comprehensive sidebar-first implementation prompt.
|
||||||
|
- Captured mobile collapsed sidebar requirements (hamburger + five quick-access icons).
|
||||||
|
- Captured IA/naming migration from legacy labels to content-true labels.
|
||||||
|
### Tasks reset: none
|
||||||
|
### Tasks added:
|
||||||
|
- Remove top navbar and eliminate hidden top scroll space artifact.
|
||||||
|
- Move primary nav into sidebar with canonical labels.
|
||||||
|
- Add `Navigation` subgroup and contextual links.
|
||||||
|
- Implement collapsed-by-default mobile sidebar with quick icons and expandable full menu.
|
||||||
|
- Apply GP-style iconography to recruiter-friendly labels.
|
||||||
|
### Context for next iteration:
|
||||||
|
- Treat this as a structural layout update, not a cosmetic tweak.
|
||||||
|
- Prioritize fixing offset/height logic that currently reveals hidden space above sidebar content.
|
||||||
|
- Keep text labels aligned to portfolio content while allowing metaphor-based icons.
|
||||||
|
### New guardrails added: none
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ export function DashboardLayout() {
|
|||||||
{/* PatientSummaryTile — full width (includes Latest Results subsection) */}
|
{/* PatientSummaryTile — full width (includes Latest Results subsection) */}
|
||||||
<PatientSummaryTile />
|
<PatientSummaryTile />
|
||||||
|
|
||||||
{/* ProjectsTile — half width */}
|
{/* ProjectsTile — full width */}
|
||||||
<ProjectsTile />
|
<ProjectsTile />
|
||||||
|
|
||||||
{/* Patient Pathway — parent section with constellation graph + subsections */}
|
{/* Patient Pathway — parent section with constellation graph + subsections */}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const sections: NavSection[] = [
|
|||||||
{ id: 'overview', label: 'Overview', tileId: 'patient-summary' },
|
{ id: 'overview', label: 'Overview', tileId: 'patient-summary' },
|
||||||
{ id: 'skills', label: 'Skills', tileId: 'section-skills' },
|
{ id: 'skills', label: 'Skills', tileId: 'section-skills' },
|
||||||
{ id: 'experience', label: 'Experience', tileId: 'section-experience' },
|
{ id: 'experience', label: 'Experience', tileId: 'section-experience' },
|
||||||
{ id: 'projects', label: 'Projects', tileId: 'projects' },
|
{ id: 'projects', label: 'Significant Interventions', tileId: 'projects' },
|
||||||
{ id: 'education', label: 'Education', tileId: 'section-education' },
|
{ id: 'education', label: 'Education', tileId: 'section-education' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { investigations } from '@/data/investigations'
|
import { investigations } from '@/data/investigations'
|
||||||
import { Card, CardHeader } from '../Card'
|
import { Card, CardHeader } from '../Card'
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||||
@@ -12,10 +12,19 @@ const statusColorMap: Record<string, string> = {
|
|||||||
|
|
||||||
interface ProjectItemProps {
|
interface ProjectItemProps {
|
||||||
project: Investigation
|
project: Investigation
|
||||||
|
slideWidth: string
|
||||||
|
cardMinHeight: number
|
||||||
|
thumbnailHeight: number
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectItem({ project, onClick }: ProjectItemProps) {
|
function ProjectItem({
|
||||||
|
project,
|
||||||
|
slideWidth,
|
||||||
|
cardMinHeight,
|
||||||
|
thumbnailHeight,
|
||||||
|
onClick,
|
||||||
|
}: ProjectItemProps) {
|
||||||
const dotColor = statusColorMap[project.status] || '#0D6E6E'
|
const dotColor = statusColorMap[project.status] || '#0D6E6E'
|
||||||
const isLive = project.status === 'Live'
|
const isLive = project.status === 'Live'
|
||||||
|
|
||||||
@@ -30,6 +39,12 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: `0 0 ${slideWidth}`,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -38,11 +53,12 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
gap: '10px',
|
||||||
background: 'var(--surface)',
|
background: 'var(--surface)',
|
||||||
border: '1px solid var(--border-light)',
|
border: '1px solid var(--border-light)',
|
||||||
borderRadius: 'var(--radius-sm)',
|
borderRadius: 'var(--radius-sm)',
|
||||||
padding: '12px 16px',
|
padding: '12px',
|
||||||
minHeight: '44px',
|
minHeight: `${cardMinHeight}px`,
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||||
@@ -56,14 +72,41 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
|
|||||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||||
e.currentTarget.style.boxShadow = 'none'
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
}}
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(26,43,42,0.08)'
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||||
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Row: status dot + name + year */}
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: `${thumbnailHeight}px`,
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid var(--border-light)',
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, rgba(19, 94, 94, 0.12), rgba(212, 171, 46, 0.18))',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontFamily: 'var(--font-geist-mono)',
|
||||||
|
fontSize: '10px',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
color: 'var(--text-tertiary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Thumbnail Pending
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
marginBottom: '8px',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -91,7 +134,6 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tech stack tags */}
|
|
||||||
{project.techStack && project.techStack.length > 0 && (
|
{project.techStack && project.techStack.length > 0 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -119,25 +161,187 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectsTile() {
|
export function ProjectsTile() {
|
||||||
const { openPanel } = useDetailPanel()
|
const { openPanel } = useDetailPanel()
|
||||||
|
const viewportRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const trackRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const firstSetRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const offsetRef = useRef(0)
|
||||||
|
const isPausedRef = useRef(false)
|
||||||
|
const [viewportWidth, setViewportWidth] = useState(1200)
|
||||||
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() =>
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const viewportEl = viewportRef.current
|
||||||
|
if (!viewportEl || typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWidth = () => {
|
||||||
|
const nextWidth = viewportEl.clientWidth
|
||||||
|
if (nextWidth > 0) {
|
||||||
|
setViewportWidth(nextWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateWidth()
|
||||||
|
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
const observer = new ResizeObserver(() => updateWidth())
|
||||||
|
observer.observe(viewportEl)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateWidth)
|
||||||
|
return () => window.removeEventListener('resize', updateWidth)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||||
|
const syncMotionPreference = () => setPrefersReducedMotion(mediaQuery.matches)
|
||||||
|
|
||||||
|
syncMotionPreference()
|
||||||
|
mediaQuery.addEventListener('change', syncMotionPreference)
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener('change', syncMotionPreference)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trackEl = trackRef.current
|
||||||
|
const firstSetEl = firstSetRef.current
|
||||||
|
if (!trackEl || !firstSetEl || prefersReducedMotion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFrameId = 0
|
||||||
|
let lastTime = 0
|
||||||
|
const speedPxPerSecond = viewportWidth < 768 ? 18 : 24
|
||||||
|
|
||||||
|
const tick = (timestamp: number) => {
|
||||||
|
if (!lastTime) {
|
||||||
|
lastTime = timestamp
|
||||||
|
}
|
||||||
|
const deltaSeconds = (timestamp - lastTime) / 1000
|
||||||
|
lastTime = timestamp
|
||||||
|
|
||||||
|
if (!isPausedRef.current) {
|
||||||
|
const setWidth = firstSetEl.offsetWidth
|
||||||
|
if (setWidth > 0) {
|
||||||
|
offsetRef.current += speedPxPerSecond * deltaSeconds
|
||||||
|
if (offsetRef.current >= setWidth) {
|
||||||
|
offsetRef.current -= setWidth
|
||||||
|
}
|
||||||
|
trackEl.style.transform = `translate3d(-${offsetRef.current}px, 0, 0)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = window.requestAnimationFrame(tick)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = window.requestAnimationFrame(tick)
|
||||||
|
return () => window.cancelAnimationFrame(animationFrameId)
|
||||||
|
}, [prefersReducedMotion, viewportWidth])
|
||||||
|
|
||||||
|
const cardsPerView = useMemo(() => {
|
||||||
|
if (viewportWidth < 768) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 4
|
||||||
|
}, [viewportWidth])
|
||||||
|
|
||||||
|
const slideWidth = useMemo(() => {
|
||||||
|
const gap = 12
|
||||||
|
const totalGap = (cardsPerView - 1) * gap
|
||||||
|
const computedWidth = (viewportWidth - totalGap) / cardsPerView
|
||||||
|
return `${Math.max(computedWidth, 0)}px`
|
||||||
|
}, [cardsPerView, viewportWidth])
|
||||||
|
|
||||||
|
const cardMinHeight = useMemo(() => {
|
||||||
|
if (viewportWidth < 640) {
|
||||||
|
return 168
|
||||||
|
}
|
||||||
|
if (viewportWidth < 1024) {
|
||||||
|
return 182
|
||||||
|
}
|
||||||
|
if (viewportWidth < 1440) {
|
||||||
|
return 196
|
||||||
|
}
|
||||||
|
return 214
|
||||||
|
}, [viewportWidth])
|
||||||
|
|
||||||
|
const thumbnailHeight = useMemo(() => {
|
||||||
|
if (viewportWidth < 640) {
|
||||||
|
return 62
|
||||||
|
}
|
||||||
|
if (viewportWidth < 1024) {
|
||||||
|
return 68
|
||||||
|
}
|
||||||
|
if (viewportWidth < 1440) {
|
||||||
|
return 76
|
||||||
|
}
|
||||||
|
return 84
|
||||||
|
}, [viewportWidth])
|
||||||
|
|
||||||
|
const setPaused = (value: boolean) => {
|
||||||
|
isPausedRef.current = value
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card tileId="projects">
|
<Card full tileId="projects">
|
||||||
<CardHeader dotColor="amber" title="ACTIVE PROJECTS" />
|
<CardHeader dotColor="amber" title="SIGNIFICANT INTERVENTIONS" />
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
<div
|
||||||
|
ref={viewportRef}
|
||||||
|
style={{ overflow: 'hidden' }}
|
||||||
|
onMouseEnter={() => setPaused(true)}
|
||||||
|
onMouseLeave={() => setPaused(false)}
|
||||||
|
onFocusCapture={() => setPaused(true)}
|
||||||
|
onBlurCapture={(event) => {
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
||||||
|
setPaused(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={trackRef}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: 'max-content',
|
||||||
|
willChange: 'transform',
|
||||||
|
transform: 'translate3d(0, 0, 0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[0, 1].map((setIndex) => (
|
||||||
|
<div
|
||||||
|
key={setIndex}
|
||||||
|
ref={setIndex === 0 ? firstSetRef : undefined}
|
||||||
|
style={{ display: 'flex', gap: '12px', paddingRight: '12px', flexShrink: 0 }}
|
||||||
|
>
|
||||||
{investigations.map((project) => (
|
{investigations.map((project) => (
|
||||||
<ProjectItem
|
<ProjectItem
|
||||||
key={project.id}
|
key={`${setIndex}-${project.id}`}
|
||||||
project={project}
|
project={project}
|
||||||
|
slideWidth={slideWidth}
|
||||||
|
cardMinHeight={cardMinHeight}
|
||||||
|
thumbnailHeight={thumbnailHeight}
|
||||||
onClick={() => openPanel({ type: 'project', investigation: project })}
|
onClick={() => openPanel({ type: 'project', investigation: project })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -390,7 +390,7 @@ html {
|
|||||||
/* Desktop: 2 columns */
|
/* Desktop: 2 columns */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.pathway-columns {
|
.pathway-columns {
|
||||||
grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -7,7 +7,7 @@ import { skills } from '@/data/skills'
|
|||||||
import { kpis } from '@/data/kpis'
|
import { kpis } from '@/data/kpis'
|
||||||
import type { DetailPanelContent } from '@/types/pmr'
|
import type { DetailPanelContent } from '@/types/pmr'
|
||||||
|
|
||||||
export type PaletteSection = 'Experience' | 'Core Skills' | 'Active Projects' | 'Achievements' | 'Education' | 'Quick Actions'
|
export type PaletteSection = 'Experience' | 'Core Skills' | 'Significant Interventions' | 'Achievements' | 'Education' | 'Quick Actions'
|
||||||
|
|
||||||
export type PaletteAction =
|
export type PaletteAction =
|
||||||
| { type: 'scroll'; tileId: string }
|
| { type: 'scroll'; tileId: string }
|
||||||
@@ -61,13 +61,13 @@ export function buildPaletteData(): PaletteItem[] {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Active Projects — all 5 investigations from investigations.ts
|
// Significant Interventions — all 5 investigations from investigations.ts
|
||||||
investigations.forEach((inv) => {
|
investigations.forEach((inv) => {
|
||||||
items.push({
|
items.push({
|
||||||
id: `proj-${inv.id}`,
|
id: `proj-${inv.id}`,
|
||||||
title: inv.name,
|
title: inv.name,
|
||||||
subtitle: `${inv.methodology.split('.')[0]} \u00b7 ${inv.requestedYear}`,
|
subtitle: `${inv.methodology.split('.')[0]} \u00b7 ${inv.requestedYear}`,
|
||||||
section: 'Active Projects',
|
section: 'Significant Interventions',
|
||||||
iconVariant: 'amber',
|
iconVariant: 'amber',
|
||||||
iconType: 'project',
|
iconType: 'project',
|
||||||
keywords: `${inv.name.toLowerCase()} ${inv.methodology.toLowerCase()} ${inv.techStack.join(' ').toLowerCase()} ${inv.requestedYear}`,
|
keywords: `${inv.name.toLowerCase()} ${inv.methodology.toLowerCase()} ${inv.techStack.join(' ').toLowerCase()} ${inv.requestedYear}`,
|
||||||
@@ -218,7 +218,7 @@ export function buildSearchIndex(items: PaletteItem[]): Fuse<PaletteItem> {
|
|||||||
const SECTION_ORDER: PaletteSection[] = [
|
const SECTION_ORDER: PaletteSection[] = [
|
||||||
'Experience',
|
'Experience',
|
||||||
'Core Skills',
|
'Core Skills',
|
||||||
'Active Projects',
|
'Significant Interventions',
|
||||||
'Achievements',
|
'Achievements',
|
||||||
'Education',
|
'Education',
|
||||||
'Quick Actions',
|
'Quick Actions',
|
||||||
@@ -332,7 +332,7 @@ export function buildEmbeddingTexts(): Array<{ id: string; text: string }> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Investigations (Active Projects) — enriched with role context and cross-references
|
// Investigations (Significant Interventions) — enriched with role context and cross-references
|
||||||
const projectContextMap: Record<string, string> = {
|
const projectContextMap: Record<string, string> = {
|
||||||
'inv-pharmetrics': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Provides self-serve analytics for budget holders across the integrated care system. Live at medicines.charlwood.xyz.',
|
'inv-pharmetrics': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Provides self-serve analytics for budget holders across the integrated care system. Live at medicines.charlwood.xyz.',
|
||||||
'inv-switching-algorithm': 'Built during Interim Head role at NHS Norfolk & Waveney ICB. Uses real-world GP prescribing data to auto-identify patients on expensive drugs suitable for cost-effective alternatives. Compressed months of manual analysis into 3 days. Includes novel GP payment system linking incentive rewards to prescribing savings.',
|
'inv-switching-algorithm': 'Built during Interim Head role at NHS Norfolk & Waveney ICB. Uses real-world GP prescribing data to auto-identify patients on expensive drugs suitable for cost-effective alternatives. Compressed months of manual analysis into 3 days. Includes novel GP payment system linking incentive rewards to prescribing savings.',
|
||||||
@@ -394,4 +394,3 @@ export function buildEmbeddingTexts(): Array<{ id: string; text: string }> {
|
|||||||
|
|
||||||
return texts
|
return texts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user