Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@
"tailwindCSS.experimental.configFile": "packages/web/src/global.css",
"files.associations": {
"*.css": "tailwindcss"
},
}
}
9 changes: 9 additions & 0 deletions packages/web/src/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { BASEPATH } from '@config/api.js';
import ProtectedGuard from '@/components/auth/ProtectedGuard.jsx';
import ProjectView from '@/components/project/ProjectView.jsx';
import { CreateOrgPage } from '@/components/org/index.js';
import MockIndex from '@/components/mock/MockIndex.jsx';
import RobinsReconcileSectionBQuestionMock from '@/components/mock/RobinsReconcileSectionBQuestionMock.jsx';

export default function AppRoutes() {
return (
Expand Down Expand Up @@ -68,6 +70,13 @@ export default function AppRoutes() {
{/* Local checklists (not org-scoped, work offline) */}
<Route path='/checklist/*' component={LocalChecklistView} />
<Route path='/checklist/:checklistId' component={LocalChecklistView} />

{/* Mock routes - public, visual-only wireframes */}
<Route path='/mock' component={MockIndex} />
<Route
path='/mock/robins-reconcile-section-b-question'
component={RobinsReconcileSectionBQuestionMock}
/>
</Route>
<Route path='*' component={NotFoundPage} />
</Router>
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/components/checklist/ChecklistWithPdf.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function ChecklistWithPdf(props) {
// props.selectedPdfId - currently selected PDF ID
// props.onPdfSelect - handler for PDF selection change
// props.getQuestionNote - function to get Y.Text for a question note
// props.getRobinsText - function to get Y.Text for a ROBINS-I free-text field
// props.pdfUrl - optional PDF URL (for server-hosted PDFs)

return (
Expand All @@ -44,6 +45,7 @@ export default function ChecklistWithPdf(props) {
onUpdate={props.onUpdate}
readOnly={props.readOnly}
getQuestionNote={props.getQuestionNote}
getRobinsText={props.getRobinsText}
/>

{/* Second panel: PDF Viewer */}
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/components/checklist/ChecklistYjsWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function ChecklistYjsWrapper() {
getChecklistData,
addPdfToStudy,
getQuestionNote,
getRobinsText,
} = useProject(params.projectId);

// Set active project for action store
Expand Down Expand Up @@ -460,6 +461,9 @@ export default function ChecklistYjsWrapper() {
getQuestionNote={questionKey =>
getQuestionNote(params.studyId, params.checklistId, questionKey)
}
getRobinsText={(sectionKey, fieldKey, questionKey) =>
getRobinsText(params.studyId, params.checklistId, sectionKey, fieldKey, questionKey)
}
/>
</Show>
</>
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/components/checklist/GenericChecklist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ROBINSIChecklist } from '@/components/checklist/ROBINSIChecklist/index.
* @param {Function} props.onUpdate - Callback for checklist updates
* @param {boolean} [props.readOnly] - Whether the checklist is read-only
* @param {Function} [props.getQuestionNote] - Function to get Y.Text for a question note
* @param {Function} [props.getRobinsText] - Function to get Y.Text for a ROBINS-I free-text field
*/
export default function GenericChecklist(props) {
// Determine the checklist type from props or state
Expand Down Expand Up @@ -55,6 +56,7 @@ export default function GenericChecklist(props) {
showComments={true}
showLegend={true}
readOnly={props.readOnly}
getRobinsText={props.getRobinsText}
/>
</Show>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { For } from 'solid-js';
import { For, Show } from 'solid-js';
import { ROB_JUDGEMENTS, BIAS_DIRECTIONS, DOMAIN1_DIRECTIONS } from './checklist-map.js';

/**
* Domain judgement selector with risk of bias level and optional direction
* Supports auto-first mode: in auto mode, buttons are visually secondary and clicking switches to manual
*
* @param {Object} props
* @param {string} props.domainId - Unique domain identifier
* @param {string} props.judgement - Current judgement value
Expand All @@ -12,11 +14,20 @@ import { ROB_JUDGEMENTS, BIAS_DIRECTIONS, DOMAIN1_DIRECTIONS } from './checklist
* @param {boolean} [props.showDirection] - Whether to show direction selector
* @param {boolean} [props.isDomain1] - Whether this is Domain 1 (uses limited direction options)
* @param {boolean} [props.disabled] - Whether the selector is disabled
* @param {boolean} [props.isAutoMode] - Whether in auto mode (buttons are secondary, clicking switches to manual)
*/
export function DomainJudgement(props) {
const directionOptions = () => (props.isDomain1 ? DOMAIN1_DIRECTIONS : BIAS_DIRECTIONS);

const getJudgementColor = judgement => {
const getJudgementColor = (judgement, isSelected) => {
if (!isSelected) {
// Unselected state - slightly dimmed in auto mode
return props.isAutoMode ?
'border-gray-200 bg-gray-50 text-gray-500 hover:border-gray-300 hover:bg-white'
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300';
}

// Selected state
switch (judgement) {
case 'Low':
return 'bg-green-100 border-green-400 text-green-800';
Expand All @@ -33,41 +44,42 @@ export function DomainJudgement(props) {
}
};

// Shorten long judgement labels for display
const getShortLabel = judgement => {
if (judgement === 'Low (except for concerns about uncontrolled confounding)') {
return 'Low (except confounding)';
}
return judgement;
};

return (
<div class='mt-4 rounded-lg bg-gray-50 p-4'>
{/* Risk of bias judgement */}
<div class='mb-3'>
<div class='mb-2 text-sm font-medium text-gray-700'>Risk of bias judgement</div>
<div class='flex flex-wrap gap-2'>
<For each={ROB_JUDGEMENTS}>
{judgement => {
const isSelected = () => props.judgement === judgement;
return (
<button
type='button'
onClick={() => {
if (props.disabled) return;
// Toggle: deselect if already selected, otherwise select
props.onJudgementChange(isSelected() ? null : judgement);
}}
disabled={props.disabled}
class={`inline-flex items-center justify-center rounded-md border-2 px-3 py-1.5 text-sm font-medium transition-colors ${props.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} ${
isSelected() ?
getJudgementColor(judgement)
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300'
} `}
>
{judgement}
</button>
);
}}
</For>
</div>
<div>
{/* Risk of bias judgement buttons */}
<div class='flex flex-wrap gap-2'>
<For each={ROB_JUDGEMENTS}>
{judgement => {
const isSelected = () => props.judgement === judgement;
return (
<button
type='button'
onClick={() => {
if (props.disabled) return;
// In both modes: select the judgement (switches to manual via parent)
props.onJudgementChange(isSelected() ? null : judgement);
}}
disabled={props.disabled}
class={`inline-flex items-center justify-center rounded-md border-2 px-3 py-1.5 text-sm font-medium transition-colors ${props.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} ${getJudgementColor(judgement, isSelected())}`}
>
{getShortLabel(judgement)}
</button>
);
}}
</For>
</div>

{/* Direction of bias (optional) */}
{props.showDirection && (
<div>
<Show when={props.showDirection}>
<div class='mt-3'>
<div class='mb-2 text-sm font-medium text-gray-700'>
Predicted direction of bias
<span class='ml-1 font-normal text-gray-400'>(optional)</span>
Expand All @@ -81,15 +93,14 @@ export function DomainJudgement(props) {
type='button'
onClick={() => {
if (props.disabled) return;
// Toggle: deselect if already selected, otherwise select
props.onDirectionChange?.(isSelected() ? null : direction);
}}
disabled={props.disabled}
class={`inline-flex items-center justify-center rounded border px-2 py-1 text-xs font-medium transition-colors ${props.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} ${
isSelected() ?
'border-blue-400 bg-blue-100 text-blue-800'
: 'border-gray-200 bg-white text-gray-500 hover:border-gray-300'
} `}
}`}
>
{direction}
</button>
Expand All @@ -98,7 +109,7 @@ export function DomainJudgement(props) {
</For>
</div>
</div>
)}
</Show>
</div>
);
}
Expand All @@ -125,9 +136,17 @@ export function JudgementBadge(props) {
}
};

// Shorten long judgement labels for badges
const getShortLabel = () => {
if (props.judgement === 'Low (except for concerns about uncontrolled confounding)') {
return 'Low (except confounding)';
}
return props.judgement || 'Not assessed';
};

return (
<span class={`inline-flex rounded px-2 py-0.5 text-xs font-medium ${getColor()}`}>
{props.judgement || 'Not assessed'}
{getShortLabel()}
</span>
);
}
Expand Down
Loading