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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added
- Bakta `.gbff` and GFF3 feature import support.
- Reference GenBank CDS annotations in both the canvas preview and SVG export.
- A desktop floating Ring Configuration panel with drag, resize, minimise, dock, close and session-persisted dimensions.
- A plot centre control and correctly scaled legend dragging.
- Direct HEX colour entry for comparison rings.
- Explanatory average-identity tooltip in the statistics panel.
Expand All @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Regression coverage for GFF3, file access, editable SVG output, and annotation editing/paste/deletion.

### Changed
- Replaced the temporary editor preview action with a top-level read-only **Viewer** at `/viewer` for public URLs and local result/session files, including the result statistics and saved BLAST-output downloads.
- Settings and output panels now remain usable together on laptop screens.
- Annotation editing now preserves manual edits and spreadsheet paste, with reliable row deletion and reset.
- SVG and PNG exports preserve the preview zoom, pan, and dragged legend positions.
Expand All @@ -25,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Production DOMPurify is pinned to the patched release.

### Removed
- Automatic display of CDS annotations embedded in GenBank and GBFF reference files; annotations are now added explicitly as custom ring overlays.
- `@biowasm/aioli` dependency — unused since BRIGx ships its own custom WASM build.

### Moved
Expand Down
8 changes: 4 additions & 4 deletions components/AnnotationEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export default function AnnotationEditor({

return (
<div
className="fixed inset-0 flex items-center justify-center z-50 p-4"
className="annotation-editor-backdrop fixed inset-0 flex items-center justify-center p-4"
style={{ background: 'rgba(0, 0, 0, 0.6)' }}
role="dialog"
aria-modal="true"
Expand Down Expand Up @@ -302,10 +302,10 @@ export default function AnnotationEditor({
</div>
)}

<div className="flex-1 overflow-auto p-4" style={{ minHeight: '400px' }}>
<table className="w-full border-collapse text-sm" aria-label="Annotations">
<div className="flex-1 overflow-auto px-4 pb-4" style={{ minHeight: '400px' }}>
<table className="annotation-table w-full border-separate border-spacing-0 text-sm" aria-label="Annotations">
<caption className="sr-only">Editable annotations. Use the checkboxes to select rows for deletion.</caption>
<thead className="sticky top-0 z-10" style={{ background: 'var(--gx-bg-alt)' }}>
<thead>
<tr>
<th className="border p-2 w-12" style={{ borderColor: 'var(--gx-border)' }} scope="col">
<input
Expand Down
6 changes: 1 addition & 5 deletions components/BrowserSessionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
interface BrowserSessionBarProps {
onSave: () => void;
onLoad: (event: React.ChangeEvent<HTMLInputElement>) => void;
onPreview: () => void;
previewDisabled: boolean;
onReportBug: () => void;
}

export default function BrowserSessionBar({ onSave, onLoad, onPreview, previewDisabled, onReportBug }: BrowserSessionBarProps) {
export default function BrowserSessionBar({ onSave, onLoad, onReportBug }: BrowserSessionBarProps) {
return (
<nav className="browser-session-bar" aria-label="Browser session controls">
<div className="product-width browser-session-inner">
<span>Browser workspace</span>
<div>
<button type="button" onClick={onSave}>Save session</button>
<label>
Load session
<input type="file" accept=".json" onChange={onLoad} className="hidden" />
</label>
<button type="button" onClick={onPreview} disabled={previewDisabled}>Preview result</button>
<button type="button" onClick={onReportBug}>Report a bug</button>
</div>
</div>
Expand Down
19 changes: 17 additions & 2 deletions components/BugReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,23 @@ export default function BugReportModal({ debugOutput, onClose }: BugReportModalP
}),
});

const result = await response.json() as { error?: string };
if (!response.ok) throw new Error(result.error || 'The report could not be sent.');
const responseBody = await response.text();
let result: { error?: string } = {};
if (responseBody) {
try {
result = JSON.parse(responseBody) as { error?: string };
} catch {
// Some hosting and proxy errors return HTML or plain text. The HTTP
// status below is more useful to the reporter than a JSON parse error.
}
}

if (!response.ok) {
const localDevelopmentError = import.meta.env.DEV && response.status === 404
? 'Bug reports cannot be sent from the local Vite server because the Cloudflare reporting endpoint is not running. Submit the report from the deployed BRIGX site.'
: '';
throw new Error(result.error || localDevelopmentError || `The report could not be sent (HTTP ${response.status}).`);
}
setStatus('sent');
} catch (error) {
setStatus('error');
Expand Down
2 changes: 2 additions & 0 deletions components/ProductNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ const actions = (
<>
<Link to="/" className="gx-nav-link">Home</Link>
<Link to="/app" className="gx-nav-link">Web app</Link>
<Link to="/viewer" className="gx-nav-link">Viewer</Link>
</>
);

const mobileActions = (
<>
<Link to="/" className="gx-nav-dropdown-link">Home</Link>
<Link to="/app" className="gx-nav-dropdown-link">Open web app</Link>
<Link to="/viewer" className="gx-nav-dropdown-link">Viewer</Link>
</>
);

Expand Down
2 changes: 1 addition & 1 deletion components/ReferenceInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function ReferenceInput({
/>
</div>
<p className="text-xs mt-2" style={{ color: 'var(--gx-text-muted)' }}>
GenBank and GBFF CDS annotations are displayed automatically. Multi-FASTA references are joined with spacers.
Multi-FASTA references are joined with spacers.
</p>
</div>
</div>
Expand Down
17 changes: 9 additions & 8 deletions components/ResultViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Link } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import CircularPlot from '@/components/CircularPlot';
import StatisticsPanel from '@/components/StatisticsPanel';
import {
resultSnapshotFilename,
type ResultSnapshot,
} from '@/lib/resultSnapshot';

interface ResultViewerProps {
snapshot: ResultSnapshot;
mode: 'preview' | 'publication' | 'file' | 'remote';
mode: 'publication' | 'file' | 'remote';
onOpenFile?: () => void;
editUrl?: string;
}
Expand All @@ -25,18 +27,14 @@ function downloadSnapshot(snapshot: ResultSnapshot) {
export default function ResultViewer({ snapshot, mode, onOpenFile, editUrl }: ResultViewerProps) {
return (
<main className="publication-page">
<Toaster position="bottom-right" />
<header className="publication-header">
<div>
<span className="product-kicker">
{mode === 'publication' ? 'BRIGX publication' : 'BRIGX read-only result'}
</span>
{mode === 'publication' && <span className="product-kicker">BRIGX publication</span>}
<h1>{snapshot.title}</h1>
{snapshot.description && <p>{snapshot.description}</p>}
{mode === 'preview' && (
<p className="publication-note">Local preview · available in this browser for 24 hours · this URL is not shareable</p>
)}
{mode === 'remote' && (
<p className="publication-note">Loaded from a public GitHub session · read-only preview</p>
<p className="publication-note">Loaded from a public GitHub session · read-only viewer</p>
)}
</div>
<div className="publication-actions">
Expand All @@ -52,6 +50,9 @@ export default function ResultViewer({ snapshot, mode, onOpenFile, editUrl }: Re
<section className="publication-viewer" aria-label="Read-only interactive genome comparison">
<CircularPlot data={snapshot.plot} imageProperties={snapshot.imageConfig} />
</section>
<section className="publication-statistics" aria-label="Comparison statistics">
<StatisticsPanel plotData={snapshot.plot} />
</section>
</main>
);
}
62 changes: 49 additions & 13 deletions components/RingConfiguration.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


import { useState } from 'react';
import { useState, type PointerEventHandler, type ReactNode } from 'react';
import type { RingConfig, RingData } from '@/lib/types';

/** Small controlled hex input that syncs with external colour state on blur / valid input. */
Expand Down Expand Up @@ -46,14 +46,33 @@ interface RingConfigurationProps {
setRings: (rings: RingConfig[]) => void;
onEditAnnotations?: (ringId: string) => void;
ringDataList?: RingData[]; // Computed ring data (for graph stats display)
panelControls?: ReactNode;
panelMinimised?: boolean;
panelFloating?: boolean;
onPanelHeaderPointerDown?: PointerEventHandler<HTMLDivElement>;
onPanelHeaderPointerMove?: PointerEventHandler<HTMLDivElement>;
onPanelHeaderPointerUp?: PointerEventHandler<HTMLDivElement>;
onPanelHeaderPointerCancel?: PointerEventHandler<HTMLDivElement>;
}

const PRESET_COLORS = [
'#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6',
'#1abc9c', '#e67e22', '#34495e', '#16a085', '#c0392b'
];

export default function RingConfiguration({ rings, setRings, onEditAnnotations, ringDataList }: RingConfigurationProps) {
export default function RingConfiguration({
rings,
setRings,
onEditAnnotations,
ringDataList,
panelControls,
panelMinimised = false,
panelFloating = false,
onPanelHeaderPointerDown,
onPanelHeaderPointerMove,
onPanelHeaderPointerUp,
onPanelHeaderPointerCancel,
}: RingConfigurationProps) {
const [draggedRingId, setDraggedRingId] = useState<string | null>(null);
const [dragOverRingId, setDragOverRingId] = useState<string | null>(null);
const [collapsedRingIds, setCollapsedRingIds] = useState<Set<string>>(() => new Set());
Expand Down Expand Up @@ -125,25 +144,42 @@ export default function RingConfiguration({ rings, setRings, onEditAnnotations,

return (
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-center gap-2">
<div
className={`ring-configuration-header${panelFloating ? ' is-draggable' : ''}`}
onPointerDown={onPanelHeaderPointerDown}
onPointerMove={onPanelHeaderPointerMove}
onPointerUp={onPanelHeaderPointerUp}
onPointerCancel={onPanelHeaderPointerCancel}
>
<h3 className="text-sm font-semibold" style={{ color: 'var(--gx-text)' }}>Ring Configuration</h3>
<button
type="button"
onClick={addNewRing}
className="btn-secondary text-xs px-3 py-1"
>
+ Add New Ring
</button>
<div className="ring-configuration-header-actions">
{panelControls}
{!panelMinimised && (
<button
type="button"
onClick={addNewRing}
className="btn-secondary text-xs px-3 py-1"
>
+ Add New Ring
</button>
)}
</div>
</div>

{rings.length === 0 && (
{!panelMinimised && (
<p className="text-xs" style={{ color: 'var(--gx-text-muted)' }}>
Each ring can contain sequence files, graph data or a custom feature overlay. Ring order runs from the inside out.
</p>
)}

{!panelMinimised && rings.length === 0 && (
<div className="text-center py-8 rounded-lg" style={{ color: 'var(--gx-text-muted)', border: '2px dashed var(--gx-border)' }}>
<p className="mb-2">No rings configured</p>
<p className="text-sm">Click &quot;Add New Ring&quot; to start</p>
</div>
)}

<div className="space-y-3 min-w-0">
{!panelMinimised && <div className="space-y-3 min-w-0">
{rings.map((ring) => (
(() => {
const isCollapsed = collapsedRingIds.has(ring.id);
Expand Down Expand Up @@ -411,7 +447,7 @@ export default function RingConfiguration({ rings, setRings, onEditAnnotations,
);
})()
))}
</div>
</div>}
</div>
);
}
Loading
Loading