OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
@@ -1,122 +0,0 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
// const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
const isProdBuild = process.env.NODE_ENV === 'production';
|
||||
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const tailwindcss = require('tailwindcss');
|
||||
const tailwindConfigPath = path.resolve('./tailwind.config.js');
|
||||
|
||||
const cssToJavaScript = {
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
//'style-loader',
|
||||
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
|
||||
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
verbose: true,
|
||||
plugins: [
|
||||
[tailwindcss(tailwindConfigPath)],
|
||||
[autoprefixer('last 2 version', 'ie >= 11')],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
home: './src/_pages/index.tsx',
|
||||
playground: './src/_pages/playground.tsx',
|
||||
patterns: './src/_pages/patterns.tsx',
|
||||
viewer: './src/_pages/viewer.tsx',
|
||||
colors: './src/_pages/colors.tsx',
|
||||
// add other pages here
|
||||
},
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
clean: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
// plugins: [new TsconfigPathsPlugin()],
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
...(isProdBuild
|
||||
? []
|
||||
: [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: ['react-refresh/babel'],
|
||||
},
|
||||
},
|
||||
]),
|
||||
cssToJavaScript,
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: './.webpack/template.html',
|
||||
chunks: ['home'],
|
||||
filename: 'index.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './.webpack/template.html',
|
||||
chunks: ['playground'],
|
||||
filename: 'playground.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './.webpack/template.html',
|
||||
chunks: ['patterns'],
|
||||
filename: 'patterns.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './.webpack/template.html',
|
||||
chunks: ['viewer'],
|
||||
filename: 'viewer.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './.webpack/template.html',
|
||||
chunks: ['colors'],
|
||||
filename: 'colors.html',
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
}),
|
||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||
!isDevelopment &&
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'dist'),
|
||||
},
|
||||
hot: true,
|
||||
open: true,
|
||||
port: 8002,
|
||||
historyApiFallback: true,
|
||||
devMiddleware: {
|
||||
writeToDisk: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -15,13 +15,13 @@ const dataList = [
|
||||
title: 'Measurement Label',
|
||||
description: 'Description for Measurement One.',
|
||||
optionalField: 'Optional Info 1',
|
||||
details: 'Data',
|
||||
details: { primary: ['Data'], secondary: [] },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Measurement Label',
|
||||
description: 'Description for Measurement Two.',
|
||||
details: 'Data',
|
||||
details: { primary: ['Data'], secondary: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -55,64 +55,58 @@ const dataList = [
|
||||
id: 6,
|
||||
title: 'Linear',
|
||||
description: 'Description for Linear.',
|
||||
details: '49.2 mm',
|
||||
series: 'S:2 I:1',
|
||||
details: { primary: ['49.2 mm'], secondary: ['S2 I:1'] },
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: 'Bidirectional',
|
||||
description: 'Description for Bidirectional.',
|
||||
details: 'L: 34.5 mm\nW: 23.0 mm',
|
||||
series: 'S:2 I:2',
|
||||
details: { primary: ['L: 34.5 mm', 'W: 23.0 mm'], secondary: ['S:2 I:2'] },
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: 'Ellipse',
|
||||
description: 'Description for Ellipse.',
|
||||
details: '2641 mm²\nMax: 1087 HU',
|
||||
series: 'S:2 I:4',
|
||||
details: { primary: ['2641 mm²', 'Max: 1087 HU'], secondary: ['S:2 I:4'] },
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: 'Rectangle',
|
||||
description: 'Description for Rectangle.',
|
||||
details: '1426 mm²\nMax: 718 HU',
|
||||
series: 'S:2 I:5',
|
||||
details: { primary: ['1426 mm²', 'Max: 718 HU'], secondary: ['S:2 I:5'] },
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: 'Circle',
|
||||
description: 'Description for Circle.',
|
||||
details: '7339 mm²\nMax: 871 HU',
|
||||
series: 'S:2 I:6',
|
||||
details: { primary: ['7339 mm²', 'Max: 871 HU'], secondary: ['S:2 I:6'] },
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: 'Freehand ROI',
|
||||
description: 'Description for Freehand ROI.',
|
||||
details: 'Mean: 215 HU\nMax: 947 HU\nArea: 839 mm²',
|
||||
series: 'S:2 I:7',
|
||||
details: {
|
||||
primary: ['Mean: 215 HU', 'Max: 947 HU', 'Area: 839 mm²'],
|
||||
secondary: ['S:2 I:7', 'S:3 I:7'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: 'Spline Tool',
|
||||
description: 'Description for Spline Tool.',
|
||||
details: 'Area: 203 mm²',
|
||||
series: 'S:2 I:8',
|
||||
details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:8'] },
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
title: 'Livewire Tool',
|
||||
description: 'Description for Livewire Tool.',
|
||||
details: '',
|
||||
series: 'S:2 I:3',
|
||||
details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:3'] },
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: 'Annotation Lorem ipsum dolor sit amet long measurement name continues here',
|
||||
description: 'Description for Annotation.',
|
||||
details: '',
|
||||
series: 'S:2 I:3',
|
||||
details: { primary: ['Area: 203 mm²'], secondary: ['S:2 I:3'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -213,21 +207,21 @@ const dataList = [
|
||||
title: 'Segment 1',
|
||||
colorHex: '#FF6F61',
|
||||
description: 'Description for Segmentation One.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
title: 'Segment 2',
|
||||
colorHex: '#00CED1',
|
||||
description: 'Description for Segmentation Two.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
title: 'Segment 3',
|
||||
colorHex: '#88B04B',
|
||||
description: 'Description for Segmentation One.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -239,21 +233,21 @@ const dataList = [
|
||||
title: 'Segment A',
|
||||
colorHex: '#FF6F61',
|
||||
description: 'Description for Segmentation One.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
title: 'Segment B',
|
||||
colorHex: '#00CED1',
|
||||
description: 'Description for Segmentation Two.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
{
|
||||
id: 34,
|
||||
title: 'Segment C',
|
||||
colorHex: '#88B04B',
|
||||
description: 'Description for Segmentation One.',
|
||||
details: 'SUV Peak: NaN\nVolume: 21.56mm³',
|
||||
details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mm³'], secondary: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "3.2.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"sanitize-html": "^2.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } from './Command';
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
} from './Command';
|
||||
|
||||
export {
|
||||
Command,
|
||||
|
||||
@@ -7,193 +7,321 @@ import {
|
||||
DropdownMenuItem,
|
||||
} from '../../components/DropdownMenu';
|
||||
import { Icons } from '../../components/Icons/Icons';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} from '../../components/Tooltip/Tooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
/**
|
||||
* DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data.
|
||||
* It's designed to show a numbered item with a title, optional color indicator, and expandable details.
|
||||
* The row supports various interactive features like visibility toggling, locking, and contextual actions.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* ```tsx
|
||||
* <DataRow
|
||||
* number={1}
|
||||
* title="My Item"
|
||||
* details={{
|
||||
* primary: ["Main detail", " Sub detail"],
|
||||
* secondary: []
|
||||
* }}
|
||||
* isVisible={true}
|
||||
* isLocked={false}
|
||||
* onToggleVisibility={() => {}}
|
||||
* onToggleLocked={() => {}}
|
||||
* onRename={() => {}}
|
||||
* onDelete={() => {}}
|
||||
* onColor={() => {}}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Props for the DataRow component
|
||||
* @interface DataRowProps
|
||||
* @property {number} number - The display number/index of the row
|
||||
* @property {string} title - The main text label for the row
|
||||
* @property {boolean} disableEditing - When true, prevents rename and delete operations
|
||||
* @property {string} [colorHex] - Optional hex color code to display a color indicator
|
||||
* @property {Object} [details] - Optional hierarchical details to display below the row
|
||||
* @property {string[]} details.primary - Primary details shown immediately below the row
|
||||
* @property {string[]} details.secondary - Secondary details (currently unused)
|
||||
* @property {boolean} [isSelected] - Whether the row is currently selected
|
||||
* @property {() => void} [onSelect] - Callback when the row is clicked/selected
|
||||
* @property {boolean} isVisible - Controls the row's visibility state
|
||||
* @property {() => void} onToggleVisibility - Callback to toggle visibility
|
||||
* @property {boolean} isLocked - Controls the row's locked state
|
||||
* @property {() => void} onToggleLocked - Callback to toggle locked state
|
||||
* @property {() => void} onRename - Callback when rename is requested
|
||||
* @property {() => void} onDelete - Callback when delete is requested
|
||||
* @property {() => void} onColor - Callback when color change is requested
|
||||
*/
|
||||
interface DataRowProps {
|
||||
number: number;
|
||||
title: string;
|
||||
disableEditing: boolean;
|
||||
description: string;
|
||||
optionalField?: string;
|
||||
colorHex?: string;
|
||||
details?: string;
|
||||
series?: string;
|
||||
actionOptions: string[];
|
||||
onAction: (action: string) => void;
|
||||
details?: { primary: string[]; secondary: string[] };
|
||||
//
|
||||
isSelected?: boolean;
|
||||
onSelect?: () => void;
|
||||
//
|
||||
isVisible: boolean;
|
||||
onToggleVisibility: () => void;
|
||||
//
|
||||
isLocked: boolean;
|
||||
onToggleLocked: () => void;
|
||||
//
|
||||
title: string;
|
||||
onRename: () => void;
|
||||
//
|
||||
onDelete: () => void;
|
||||
//
|
||||
colorHex?: string;
|
||||
onColor: () => void;
|
||||
}
|
||||
|
||||
const DataRow: React.FC<DataRowProps> = ({
|
||||
number,
|
||||
title,
|
||||
description,
|
||||
optionalField,
|
||||
colorHex,
|
||||
details,
|
||||
series,
|
||||
actionOptions,
|
||||
onAction,
|
||||
isSelected = false,
|
||||
onSelect,
|
||||
isLocked,
|
||||
onToggleVisibility,
|
||||
onToggleLocked,
|
||||
onRename,
|
||||
onDelete,
|
||||
onColor,
|
||||
isSelected = false,
|
||||
isVisible = true,
|
||||
disableEditing = false,
|
||||
}) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const isTitleLong = title.length > 25;
|
||||
const isTitleLong = title?.length > 25;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
{' '}
|
||||
{/* Ensure TooltipProvider wraps the component */}
|
||||
<div className="flex flex-col">
|
||||
{/* Row 1 with 'group' class to enable group-hover */}
|
||||
const handleAction = (action: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
switch (action) {
|
||||
case 'Rename':
|
||||
onRename();
|
||||
break;
|
||||
case 'Lock':
|
||||
onToggleLocked();
|
||||
break;
|
||||
case 'Delete':
|
||||
onDelete();
|
||||
break;
|
||||
case 'Color':
|
||||
onColor();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const decodeHTML = (html: string) => {
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = html;
|
||||
return txt.value;
|
||||
};
|
||||
|
||||
const renderDetailText = (text: string, indent: number = 0) => {
|
||||
const indentation = ' '.repeat(indent);
|
||||
if (text === '') {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center ${
|
||||
isSelected ? 'bg-popover' : 'bg-muted'
|
||||
} group relative cursor-pointer ${isVisible ? '' : 'opacity-60'}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{/* Hover Overlay */}
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
|
||||
|
||||
{/* Number Box */}
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-sm ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
|
||||
{/* Color Circle (Optional) */}
|
||||
{colorHex && (
|
||||
<div className="flex h-7 w-5 items-center justify-center">
|
||||
<span
|
||||
className="ml-2 h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: colorHex }}
|
||||
></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Label with Conditional Tooltip */}
|
||||
<div className="ml-2 flex-1 overflow-hidden">
|
||||
{isTitleLong ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={`cursor-default text-sm ${
|
||||
isSelected ? 'text-highlight' : 'text-muted-foreground'
|
||||
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="center"
|
||||
>
|
||||
{title}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
isSelected ? 'text-highlight' : 'text-muted-foreground'
|
||||
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions and Visibility Toggle */}
|
||||
<div className="relative ml-2 flex items-center space-x-1">
|
||||
{/* Visibility Toggle Icon */}
|
||||
{isVisible ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
className={`h-6 w-6 transition-opacity ${
|
||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
aria-label="Hide"
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent row selection on button click
|
||||
setIsVisible(false);
|
||||
}}
|
||||
>
|
||||
<Icons.Hide className="h-6 w-6" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 opacity-100"
|
||||
aria-label="Show"
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent row selection on button click
|
||||
setIsVisible(true);
|
||||
}}
|
||||
>
|
||||
<Icons.Show className="h-6 w-6" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Actions Dropdown Menu */}
|
||||
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
className={`h-6 w-6 transition-opacity ${
|
||||
isSelected || isDropdownOpen
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
aria-label="Actions"
|
||||
onClick={e => e.stopPropagation()} // Prevent row selection on button click
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">Rename</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Lock className="text-foreground" />
|
||||
<span className="pl-2">Lock</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.ColorChange className="text-foreground" />
|
||||
<span className="pl-2">Change Color</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Icons.Delete className="text-foreground" />
|
||||
<span className="pl-2">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2 (Details and Series) */}
|
||||
{(details || series) && (
|
||||
<div className="ml-7 bg-black px-2 py-1">
|
||||
{/* Updated Flex Container: Changed 'items-center' to 'items-start' */}
|
||||
<div className="text-secondary-foreground flex items-start justify-between whitespace-pre-line text-sm leading-normal">
|
||||
<span>{details}</span>
|
||||
{series && <span className="text-muted-foreground">{series}</span>}
|
||||
</div>
|
||||
</div>
|
||||
key={`empty-${indent}`}
|
||||
className="h-2"
|
||||
></div>
|
||||
); // Empty row
|
||||
}
|
||||
const cleanText = decodeHTML(text);
|
||||
return (
|
||||
<div
|
||||
key={cleanText}
|
||||
className="whitespace-pre-wrap"
|
||||
>
|
||||
{indentation}
|
||||
{cleanText.includes(':') ? (
|
||||
<>
|
||||
<span className="font-medium">{sanitizeHtml(cleanText.split(':')[0])}:</span>
|
||||
{sanitizeHtml(cleanText.split(':')[1])}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium">{sanitizeHtml(cleanText)}</span>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDetails = (details: string[]) => {
|
||||
const visibleLines = details.slice(0, 4);
|
||||
const hiddenLines = details.slice(4);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="cursor-help">
|
||||
<div className="flex flex-col space-y-1">
|
||||
{visibleLines.map((line, lineIndex) =>
|
||||
renderDetailText(line, line.startsWith(' ') ? 1 : 0)
|
||||
)}
|
||||
</div>
|
||||
{hiddenLines.length > 0 && (
|
||||
<div className="text-muted-foreground mt-1 flex items-center text-sm">
|
||||
<span>...</span>
|
||||
<Icons.Info className="mr-1 h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="max-w-md"
|
||||
>
|
||||
<div className="text-secondary-foreground flex flex-col space-y-1 text-sm leading-normal">
|
||||
{details.map((line, lineIndex) =>
|
||||
renderDetailText(line, line.startsWith(' ') ? 1 : 0)
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}>
|
||||
<div
|
||||
className={`flex items-center ${
|
||||
isSelected ? 'bg-popover' : 'bg-muted'
|
||||
} group relative cursor-pointer`}
|
||||
onClick={onSelect}
|
||||
data-cy="data-row"
|
||||
>
|
||||
{/* Hover Overlay */}
|
||||
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
|
||||
|
||||
{/* Number Box */}
|
||||
<div
|
||||
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-sm ${
|
||||
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
|
||||
{/* Color Circle (Optional) */}
|
||||
{colorHex && (
|
||||
<div className="flex h-7 w-5 items-center justify-center">
|
||||
<span
|
||||
className="ml-2 h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: colorHex }}
|
||||
></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Label with Conditional Tooltip */}
|
||||
<div className="ml-2 flex-1 overflow-hidden">
|
||||
{isTitleLong ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={`cursor-default text-sm ${
|
||||
isSelected ? 'text-highlight' : 'text-muted-foreground'
|
||||
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="center"
|
||||
>
|
||||
{title}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
isSelected ? 'text-highlight' : 'text-muted-foreground'
|
||||
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions and Visibility Toggle */}
|
||||
<div className="relative ml-2 flex items-center space-x-1">
|
||||
{/* Visibility Toggle Icon */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={`h-6 w-6 transition-opacity ${
|
||||
isSelected || !isVisible ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
aria-label={isVisible ? 'Hide' : 'Show'}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onToggleVisibility();
|
||||
}}
|
||||
>
|
||||
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
|
||||
</Button>
|
||||
|
||||
{/* Lock Icon (if needed) */}
|
||||
{isLocked && <Icons.Lock className="text-muted-foreground h-6 w-6" />}
|
||||
|
||||
{/* Actions Dropdown Menu */}
|
||||
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={`h-6 w-6 transition-opacity ${
|
||||
isSelected || isDropdownOpen ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
aria-label="Actions"
|
||||
onClick={e => e.stopPropagation()} // Prevent row selection on button click
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!disableEditing && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">Rename</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={e => handleAction('Delete', e)}>
|
||||
<Icons.Delete className="text-foreground" />
|
||||
<span className="pl-2">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
{onColor && (
|
||||
<DropdownMenuItem onClick={e => handleAction('Color', e)}>
|
||||
<Icons.ColorChange className="text-foreground" />
|
||||
<span className="pl-2">Change Color</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={e => handleAction('Lock', e)}>
|
||||
<Icons.Lock className="text-foreground" />
|
||||
<span className="pl-2">{isLocked ? 'Unlock' : 'Lock'}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details && details.primary?.length > 0 && (
|
||||
<div className="ml-7 px-2 py-2">
|
||||
<div className="text-secondary-foreground text-sm leading-normal">
|
||||
{renderDetails(details.primary)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { default as DataRow } from './DataRow';
|
||||
import DataRow from './DataRow';
|
||||
|
||||
export { DataRow };
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Represents a single data item in a list or table structure
|
||||
*
|
||||
* @interface DataItem
|
||||
*/
|
||||
export type DataItem = {
|
||||
/** Unique identifier for the data item */
|
||||
id: number;
|
||||
/** Primary text or name of the data item */
|
||||
title: string;
|
||||
/** Detailed text description of the data item */
|
||||
description: string;
|
||||
/** Additional optional field for extra information */
|
||||
optionalField?: string;
|
||||
/** Hex color code (e.g., '#FF0000') for visual representation */
|
||||
colorHex?: string;
|
||||
/** Additional details or metadata about the item */
|
||||
details?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a group of related data items with a common type
|
||||
*
|
||||
* @interface ListGroup
|
||||
*/
|
||||
export type ListGroup = {
|
||||
/** The type or category of the group */
|
||||
type: string;
|
||||
/** Array of DataItem objects belonging to this group */
|
||||
items: DataItem[];
|
||||
};
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from './Dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './Dialog';
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
|
||||
+40
-38
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icons } from '../Icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../Tooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
|
||||
|
||||
/**
|
||||
* Displays a tooltip with a list of messages of a displaySet
|
||||
@@ -14,44 +14,46 @@ const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => {
|
||||
if (messages?.size()) {
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger id={id}>
|
||||
<Icons.StatusWarning className="h-[20px] w-[20px]" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="max-w-68 text-left text-base text-white">
|
||||
<div
|
||||
className="break-normal text-base font-semibold text-blue-300"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{t('Display Set Messages')}
|
||||
</div>
|
||||
<ol
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
>
|
||||
{messages.messages.map((message, index) => (
|
||||
<li
|
||||
style={{
|
||||
marginTop: '6px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{index + 1}. {t(message.id)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<Tooltip>
|
||||
<TooltipTrigger id={id}>
|
||||
<Icons.StatusWarning
|
||||
className="h-[20px] w-[20px]"
|
||||
aria-hidden="true"
|
||||
role="presentation"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="max-w-68 text-left text-base text-white">
|
||||
<div
|
||||
className="break-normal text-base font-semibold text-blue-300"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
{t('Display Set Messages')}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<ol
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
>
|
||||
{messages.messages.map((message, index) => (
|
||||
<li
|
||||
style={{
|
||||
marginTop: '6px',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{index + 1}. {t(message.id)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Input } from '../Input';
|
||||
|
||||
interface DoubleSliderProps {
|
||||
className?: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
defaultValue?: [number, number];
|
||||
onValueChange?: (value: [number, number]) => void;
|
||||
}
|
||||
|
||||
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => {
|
||||
const [value, setValue] = React.useState<[number, number]>(defaultValue);
|
||||
|
||||
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
|
||||
|
||||
const isInteger = step % 1 === 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
// Only update if defaultValue has actually changed
|
||||
if (
|
||||
!prevDefaultValueRef.current ||
|
||||
prevDefaultValueRef.current[0] !== defaultValue[0] ||
|
||||
prevDefaultValueRef.current[1] !== defaultValue[1]
|
||||
) {
|
||||
setValue(defaultValue);
|
||||
prevDefaultValueRef.current = defaultValue;
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
const roundToStep = (num: number): number => {
|
||||
const inverse = 1 / step;
|
||||
return Math.round(num * inverse) / inverse;
|
||||
};
|
||||
|
||||
const handleSliderChange = React.useCallback(
|
||||
(newValue: number[]) => {
|
||||
const clampedValue: [number, number] = [
|
||||
roundToStep(Math.max(min, Math.min(newValue[0], max))),
|
||||
roundToStep(Math.min(max, Math.max(newValue[1], min))),
|
||||
];
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
},
|
||||
[min, max, onValueChange, step]
|
||||
);
|
||||
|
||||
const handleInputChange = React.useCallback(
|
||||
(index: 0 | 1, inputValue: string) => {
|
||||
const newValue = parseFloat(inputValue);
|
||||
if (!isNaN(newValue)) {
|
||||
const clampedValue: [number, number] = [...value];
|
||||
clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max));
|
||||
if (index === 0 && clampedValue[0] > clampedValue[1]) {
|
||||
clampedValue[1] = clampedValue[0];
|
||||
} else if (index === 1 && clampedValue[1] < clampedValue[0]) {
|
||||
clampedValue[0] = clampedValue[1];
|
||||
}
|
||||
setValue(clampedValue);
|
||||
onValueChange?.(clampedValue);
|
||||
}
|
||||
},
|
||||
[value, min, max, onValueChange, step]
|
||||
);
|
||||
|
||||
const formatValue = (val: number) => {
|
||||
return isInteger ? Math.round(val) : val;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex w-full items-center space-x-2', className)}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[0])}
|
||||
onChange={e => handleInputChange(0, e.target.value)}
|
||||
onBlur={() => handleInputChange(0, value[0].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
<SliderPrimitive.Root
|
||||
className="relative flex h-4 w-full touch-none select-none items-center"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onValueChange={handleSliderChange}
|
||||
>
|
||||
<SliderPrimitive.Track className="bg-primary/30 relative h-1 w-full grow overflow-hidden rounded-full">
|
||||
<SliderPrimitive.Range className="bg-primary absolute h-full" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="border-background bg-primary focus-visible:ring-ring block h-4 w-4 rounded-full border-2 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
<Input
|
||||
type="number"
|
||||
value={formatValue(value[1])}
|
||||
onChange={e => handleInputChange(1, e.target.value)}
|
||||
onBlur={() => handleInputChange(1, value[1].toString())}
|
||||
className="w-14"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
DoubleSlider.displayName = 'DoubleSlider';
|
||||
|
||||
export { DoubleSlider };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DoubleSlider } from './DoubleSlider';
|
||||
|
||||
export { DoubleSlider };
|
||||
@@ -20,13 +20,15 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
>(({ className, inset, children, disabled, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded px-2 py-1 text-sm outline-none',
|
||||
inset && 'pl-8',
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -80,7 +82,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded px-2 py-1 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded px-1 py-1 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '../Dialog/Dialog';
|
||||
import { ScrollArea } from '../ScrollArea/ScrollArea';
|
||||
import { Icons } from '../Icons';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
interface ErrorBoundaryError extends Error {
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
interface DefaultFallbackProps {
|
||||
error: ErrorBoundaryError;
|
||||
context: string;
|
||||
resetErrorBoundary: () => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
context?: string;
|
||||
onReset?: () => void;
|
||||
onError?: (error: ErrorBoundaryError, componentStack: string, context: string) => void;
|
||||
fallbackComponent?: React.ComponentType<DefaultFallbackProps>;
|
||||
children: React.ReactNode;
|
||||
fallbackRoute?: string | null;
|
||||
isPage?: boolean;
|
||||
}
|
||||
|
||||
const DefaultFallback = ({
|
||||
error,
|
||||
context,
|
||||
resetErrorBoundary = () => {},
|
||||
}: DefaultFallbackProps) => {
|
||||
const { t } = useTranslation('ErrorBoundary');
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`;
|
||||
const subtitle = t('Sorry, something went wrong there. Try again.');
|
||||
|
||||
const copyErrorDetails = () => {
|
||||
const errorDetails = `
|
||||
Context: ${context}
|
||||
Error Message: ${error.message}
|
||||
Stack: ${error.stack}
|
||||
`;
|
||||
navigator.clipboard.writeText(errorDetails);
|
||||
toast.success(t('Copied to clipboard'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
toast.error(title, {
|
||||
description: subtitle,
|
||||
action: {
|
||||
label: t('Show Details'),
|
||||
onClick: () => setShowDetails(true),
|
||||
},
|
||||
duration: 0,
|
||||
});
|
||||
}, [error]);
|
||||
|
||||
if (isProduction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={showDetails}
|
||||
onOpenChange={setShowDetails}
|
||||
>
|
||||
<DialogContent className="border-input h-[50vh] w-[90vw] border-2 sm:max-w-[900px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">{title}</DialogTitle>
|
||||
<DialogDescription className="text-lg">{subtitle}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[calc(90vh-120px)]">
|
||||
<div className="space-y-4 pr-4 font-mono text-base">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
copyErrorDetails();
|
||||
setShowDetails(false);
|
||||
}}
|
||||
className="text-aqua-pale hover:text-aqua-pale/80 flex items-center gap-2 rounded bg-gray-800 px-4 py-2"
|
||||
>
|
||||
<Icons.Code className="h-4 w-4" />
|
||||
{t('Copy Details')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-aqua-pale break-words text-lg">
|
||||
{t('Context')}: {context}
|
||||
</p>
|
||||
<p className="text-aqua-pale break-words text-lg">
|
||||
{t('Error Message')}: {error.message}
|
||||
</p>
|
||||
<pre className="text-aqua-pale whitespace-pre-wrap break-words rounded bg-gray-900 p-4">
|
||||
Stack: {error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorBoundary = ({
|
||||
context = 'OHIF',
|
||||
onReset = () => {},
|
||||
onError = () => {},
|
||||
fallbackComponent: FallbackComponent = DefaultFallback,
|
||||
children,
|
||||
fallbackRoute = null,
|
||||
isPage,
|
||||
}: ErrorBoundaryProps) => {
|
||||
const [error, setError] = useState<ErrorBoundaryError | null>(null);
|
||||
|
||||
const onResetHandler = () => {
|
||||
setError(null);
|
||||
onReset();
|
||||
};
|
||||
|
||||
// Add error event listener to window
|
||||
useEffect(() => {
|
||||
let errorTimeout: NodeJS.Timeout;
|
||||
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
event.preventDefault();
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = setTimeout(() => {
|
||||
setError(event.error);
|
||||
onErrorHandler(event.error, null);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRejection = (event: PromiseRejectionEvent) => {
|
||||
event.preventDefault();
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = setTimeout(() => {
|
||||
setError(event.reason);
|
||||
onErrorHandler(event.reason, null);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleError);
|
||||
window.addEventListener('unhandledrejection', handleRejection);
|
||||
|
||||
return () => {
|
||||
clearTimeout(errorTimeout);
|
||||
window.removeEventListener('error', handleError);
|
||||
window.removeEventListener('unhandledrejection', handleRejection);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onErrorHandler = (error: ErrorBoundaryError, componentStack: string) => {
|
||||
console.debug(`${context} Error Boundary`, error, componentStack, context);
|
||||
onError(error, componentStack, context);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactErrorBoundary
|
||||
fallbackRender={props => (
|
||||
<FallbackComponent
|
||||
error={props.error}
|
||||
context={context}
|
||||
resetErrorBoundary={props.resetErrorBoundary}
|
||||
/>
|
||||
)}
|
||||
onReset={onResetHandler}
|
||||
onError={onErrorHandler}
|
||||
>
|
||||
<>
|
||||
{children}
|
||||
{error && (
|
||||
<FallbackComponent
|
||||
error={error}
|
||||
context={context}
|
||||
resetErrorBoundary={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ReactErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export { ErrorBoundary };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
export { ErrorBoundary };
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
Icons,
|
||||
Button,
|
||||
} from '../';
|
||||
|
||||
import NavBar from '../NavBar';
|
||||
|
||||
// Todo: we should move this component to composition and remove props base
|
||||
|
||||
interface HeaderProps {
|
||||
children?: ReactNode;
|
||||
menuOptions: Array<{
|
||||
title: string;
|
||||
icon?: string;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
isReturnEnabled?: boolean;
|
||||
onClickReturnButton?: () => void;
|
||||
isSticky?: boolean;
|
||||
WhiteLabeling?: {
|
||||
createLogoComponentFn?: (React: any, props: any) => ReactNode;
|
||||
};
|
||||
PatientInfo?: ReactNode;
|
||||
Secondary?: ReactNode;
|
||||
}
|
||||
|
||||
function Header({
|
||||
children,
|
||||
menuOptions,
|
||||
isReturnEnabled = true,
|
||||
onClickReturnButton,
|
||||
isSticky = false,
|
||||
WhiteLabeling,
|
||||
PatientInfo,
|
||||
Secondary,
|
||||
...props
|
||||
}: HeaderProps): ReactNode {
|
||||
const { t } = useTranslation('Header');
|
||||
|
||||
const onClickReturn = () => {
|
||||
if (isReturnEnabled && onClickReturnButton) {
|
||||
onClickReturnButton();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavBar
|
||||
isSticky={isSticky}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative h-[48px] items-center">
|
||||
<div className="absolute left-0 top-1/2 flex -translate-y-1/2 items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
'mr-3 inline-flex items-center',
|
||||
isReturnEnabled && 'cursor-pointer'
|
||||
)}
|
||||
onClick={onClickReturn}
|
||||
data-cy="return-to-work-list"
|
||||
>
|
||||
{isReturnEnabled && <Icons.ChevronPatient className="text-primary-active w-8" />}
|
||||
<div className="ml-1">
|
||||
{WhiteLabeling?.createLogoComponentFn?.(React, props) || <Icons.OHIFLogo />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-[250px] h-8 -translate-y-1/2">{Secondary}</div>
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<div className="flex items-center justify-center space-x-2">{children}</div>
|
||||
</div>
|
||||
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 select-none items-center">
|
||||
{PatientInfo}
|
||||
<div className="border-primary-dark mx-1.5 h-[25px] border-r"></div>
|
||||
<div className="flex-shrink-0">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-primary-active hover:bg-primary-dark mt-2 h-full w-full"
|
||||
>
|
||||
<Icons.GearSettings />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{menuOptions.map((option, index) => {
|
||||
const IconComponent = option.icon
|
||||
? Icons[option.icon as keyof typeof Icons]
|
||||
: null;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={index}
|
||||
onSelect={option.onClick}
|
||||
className="flex items-center gap-2 py-2"
|
||||
>
|
||||
{IconComponent && (
|
||||
<span className="flex h-4 w-4 items-center justify-center">
|
||||
<IconComponent className="h-full w-full" />
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1">{option.title}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NavBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,2 @@
|
||||
import Header from './Header';
|
||||
export { Header };
|
||||
@@ -1324,6 +1324,7 @@ export const Icons = {
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>status-warning</title>
|
||||
<g
|
||||
@@ -1925,6 +1926,7 @@ export const Icons = {
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
@@ -2017,6 +2019,43 @@ export const Icons = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Refresh: (props: IconProps) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M4,12 C4,7.581722 7.581722,4 12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<polyline
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
points="12 8 12 12 15 14"
|
||||
></polyline>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Tab4D: (props: IconProps) => (
|
||||
<svg
|
||||
width="22px"
|
||||
@@ -2127,6 +2166,310 @@ export const Icons = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Plus: (props: IconProps) => (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<path d="M0 0h21v21H0z" />
|
||||
<g
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.25"
|
||||
>
|
||||
<path d="M10.5 5.5v10M15.5 10.5h-10" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Minus: (props: IconProps) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="Remove"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<line
|
||||
x1="18"
|
||||
y1="12"
|
||||
x2="6"
|
||||
y2="12"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></line>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
|
||||
FillAndOutline: (props: IconProps) => (
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="view-outline-fill"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
stroke="#348CFD"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="15"
|
||||
height="15"
|
||||
rx="1"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
fill="#348CFD"
|
||||
x="3.5"
|
||||
y="3.5"
|
||||
width="11"
|
||||
height="11"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
OutlineOnly: (props: IconProps) => (
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="view-outline"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
stroke="#348CFD"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="15"
|
||||
height="15"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
FillOnly: (props: IconProps) => (
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="view-fill"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
fill="#348CFD"
|
||||
x="2"
|
||||
y="2"
|
||||
width="14"
|
||||
height="14"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
EyeVisible: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 13"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(0 1)"
|
||||
>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="5.833"
|
||||
r="2.917"
|
||||
/>
|
||||
<path d="M19.48 5.56C18.51 4.46 14.635.417 10 .417 5.365.417 1.49 4.46.52 5.56c-.136.157-.136.39 0 .547.97 1.1 4.845 5.143 9.48 5.143 4.635 0 8.51-4.043 9.48-5.143.136-.157.136-.39 0-.547z" />
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
EyeHidden: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 20"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<g opacity=".5">
|
||||
<path
|
||||
d="M17.433 2.556c1.352.98 2.578 2.122 3.65 3.402 0 0-4.719 5.959-10.541 5.959-.933-.006-1.86-.149-2.75-.426M3.637 9.35C2.29 8.373 1.07 7.234 0 5.958 0 5.958 4.719 0 10.542 0c.773.003 1.543.103 2.291.298M6.875 5.958c0-2.025 1.642-3.666 3.667-3.666M14.208 5.958c0 2.025-1.641 3.667-3.666 3.667"
|
||||
transform="translate(1 1) translate(.458 3.208)"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M19.938 0.229L2.063 18.104"
|
||||
transform="translate(1 1)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
ViewportViews: (props: IconProps) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="icon-views"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M12.1675111,14.754511 C12.0607016,14.815163 11.9392984,14.815163 11.8324889,14.754511 L3.25173267,9.89965265 C3.09886891,9.81313583 3,9.61689546 3,9.4 C3,9.18310454 3.09886891,8.98686417 3.25173267,8.90034735 L11.8324889,4.04548902 C11.9392984,3.98483699 12.0607016,3.98483699 12.1675111,4.04548902 L20.7482673,8.90034735 C20.9011311,8.98686417 21,9.18310454 21,9.4 C21,9.61689546 20.9011311,9.81313583 20.7482673,9.89965265 L12.1675111,14.754511 Z"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M16.7792809,12.1 L20.7482673,14.3367372 C20.9011311,14.4227204 21,14.6177503 21,14.833308 C21,15.0488656 20.9011311,15.2438955 20.7482673,15.3298787 L12.1675111,20.1547916 C12.0607016,20.2150695 11.9392984,20.2150695 11.8324889,20.1547916 L3.25173267,15.3298787 C3.09886891,15.2438955 3,15.0488656 3,14.833308 C3,14.6177503 3.09886891,14.4227204 3.25173267,14.3367372 L7.2018101,12.1"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
StatusChecked: (props: IconProps) => (
|
||||
<svg
|
||||
width="14px"
|
||||
height="14px"
|
||||
viewBox="0 0 14 14"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="status-checked"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="14"
|
||||
height="14"
|
||||
></rect>
|
||||
<circle
|
||||
id="Oval"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
cx="7"
|
||||
cy="7"
|
||||
r="6"
|
||||
></circle>
|
||||
<path
|
||||
d="M9.674,5.256 L6.769,9.129 C6.63825806,9.30267425 6.43875708,9.41127411 6.22192754,9.42680302 C6.00509799,9.44233192 5.79215707,9.36327034 5.638,9.21 L4.138,7.71"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
|
||||
DicomTagBrowser: (props: IconProps) => (
|
||||
<svg
|
||||
@@ -2135,6 +2478,7 @@ export const Icons = {
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>tool-dicom-tag-browser</title>
|
||||
<g
|
||||
@@ -2202,6 +2546,311 @@ export const Icons = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
PowerOff: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 28"
|
||||
aria-labelledby="title"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<title id="title">Power Off</title>
|
||||
<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z" />
|
||||
</svg>
|
||||
),
|
||||
MultiplePatients: (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
{...props}
|
||||
>
|
||||
<title>icon-multiple-patients</title>
|
||||
<g
|
||||
id="4D-assets---final"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Artboard"
|
||||
transform="translate(-171, -280)"
|
||||
>
|
||||
<g
|
||||
id="icon-multiple-patients"
|
||||
transform="translate(171, 280)"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
></rect>
|
||||
<g
|
||||
id="Group-3"
|
||||
transform="translate(2, 1.5)"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<g
|
||||
id="patient-icon"
|
||||
transform="translate(9, 10)"
|
||||
>
|
||||
<circle
|
||||
id="Oval"
|
||||
cx="7.5"
|
||||
cy="7.5"
|
||||
r="7.5"
|
||||
></circle>
|
||||
<path
|
||||
d="M2.331,12.935 C3.39163827,12.4490078 4.47638164,12.0174468 5.581,11.642 C6.189,11.416 6.09,9.827 5.82,9.531 C4.95198021,8.59033128 4.52668888,7.3239163 4.651,6.05 C4.5727272,5.23902161 4.83519788,4.43201369 5.37555564,3.82223826 C5.9159134,3.21246283 6.68549454,2.85483523 7.5,2.835 C8.31450546,2.85483523 9.0840866,3.21246283 9.62444436,3.82223826 C10.1648021,4.43201369 10.4272728,5.23902161 10.349,6.05 C10.4733111,7.3239163 10.0480198,8.59033128 9.18,9.531 C8.91,9.831 8.811,11.416 9.419,11.642 C10.5236184,12.0174468 11.6083617,12.4490078 12.669,12.935"
|
||||
id="Path"
|
||||
></path>
|
||||
</g>
|
||||
<g id="patient-icon">
|
||||
<path
|
||||
d="M7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,8.48189915 14.8113103,9.41972745 14.4682129,10.2792028 C12.213795,10.9117859 10.3898771,12.5712406 9.53178711,14.7207972 C8.88630634,14.9028658 8.20449663,15 7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 Z"
|
||||
id="Combined-Shape"
|
||||
></path>
|
||||
<path
|
||||
d="M2.331,12.935 C3.39163827,12.4490078 4.47638164,12.0174468 5.581,11.642 C6.189,11.416 6.09,9.827 5.82,9.531 C4.95198021,8.59033128 4.52668888,7.3239163 4.651,6.05 C4.5727272,5.23902161 4.83519788,4.43201369 5.37555564,3.82223826 C5.9159134,3.21246283 6.68549454,2.85483523 7.5,2.835 C8.31450546,2.85483523 9.0840866,3.21246283 9.62444436,3.82223826 C10.1648021,4.43201369 10.4272728,5.23902161 10.349,6.05 C10.4733111,7.3239163 10.0480198,8.59033128 9.18,9.531 C8.91,9.831 8.811,11.416 9.419,11.642 C10.5236184,12.0174468 10.5236184,12.0174468 10.9701462,12.2764988"
|
||||
id="Path"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Patient: (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
{...props}
|
||||
>
|
||||
<title>icon-patient</title>
|
||||
<g
|
||||
id="Production"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Artboard"
|
||||
transform="translate(-104, -235)"
|
||||
>
|
||||
<g
|
||||
id="icon-patient"
|
||||
transform="translate(104, 235)"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
></rect>
|
||||
<circle
|
||||
id="Oval"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
cx="14"
|
||||
cy="14"
|
||||
r="9"
|
||||
></circle>
|
||||
<path
|
||||
d="M7.7972,20.522 C9.06996592,19.9388093 10.371658,19.4209361 11.6972,18.9704 C12.4268,18.6992 12.308,16.7924 11.984,16.4372 C10.9423763,15.3083975 10.4320267,13.7886996 10.5812,12.26 C10.4872726,11.2868259 10.8022375,10.3184164 11.4506668,9.58668591 C12.0990961,8.8549554 13.0225934,8.42580227 14,8.402 C14.9774066,8.42580227 15.9009039,8.8549554 16.5493332,9.58668591 C17.1977625,10.3184164 17.5127274,11.2868259 17.4188,12.26 C17.5679733,13.7886996 17.0576237,15.3083975 16.016,16.4372 C15.692,16.7972 15.5732,18.6992 16.3028,18.9704 C17.628342,19.4209361 18.9300341,19.9388093 20.2028,20.522"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
ChevronPatient: (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="29px"
|
||||
viewBox="0 0 28 29"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="Production"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Artboard"
|
||||
transform="translate(-154, -234)"
|
||||
>
|
||||
<g
|
||||
id="icon-chevron-patient"
|
||||
transform="translate(168, 248.5) scale(-1, -1) translate(-168, -248.5)translate(154, 234.5)"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
></rect>
|
||||
<polyline
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(14.5, 14) scale(-1, 1) translate(-14.5, -14)"
|
||||
points="16.5 10 12.5 14 16.5 18"
|
||||
></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
OHIFLogo: (props: IconProps) => (
|
||||
<svg
|
||||
width="138px"
|
||||
height="28px"
|
||||
viewBox="0 0 138 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="Production"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Artboard"
|
||||
transform="translate(-74, -297)"
|
||||
>
|
||||
<g
|
||||
id="toolbar-branding-open-health-imaging-foundation"
|
||||
transform="translate(74, 297)"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="138"
|
||||
height="28"
|
||||
></rect>
|
||||
<path
|
||||
d="M37.7225261,11.2237762 C39.7660251,11.2237762 41.2479518,9.64468611 41.2479518,7.11188811 C41.2479518,4.57909011 39.7660251,3 37.7225261,3 C35.6790271,3 34.1971004,4.57909011 34.1971004,7.11188811 C34.1971004,9.64468611 35.6790271,11.2237762 37.7225261,11.2237762 Z M37.8064648,10.3846154 C36.3486619,10.3846154 35.2043649,9.26464267 35.2043649,7.1958042 C35.2043649,5.12696572 36.3486619,4.00699301 37.8064648,4.00699301 C39.2642678,4.00699301 40.4085648,5.12696572 40.4085648,7.1958042 C40.4085648,9.26464267 39.2642678,10.3846154 37.8064648,10.3846154 Z M43.6645943,13.2377622 L43.6645943,10.1056298 L43.7413524,10.1056298 C43.9409235,10.4296435 44.324714,11.1393878 45.4607341,11.1393878 C46.9344898,11.1393878 47.9630485,9.95133759 47.9630485,8.0689723 C47.9630485,6.20203624 46.9344898,5.01398601 45.4453824,5.01398601 C44.2940108,5.01398601 43.9409235,5.7237303 43.7413524,6.03231478 L43.633891,6.03231478 L43.633891,5.09113213 L42.7588486,5.09113213 L42.7588486,13.2377622 L43.6645943,13.2377622 Z M45.4297631,10.3846154 C44.3559526,10.3846154 43.7661131,9.45029198 43.7661131,8.09552303 C43.7661131,6.75632615 44.3408286,5.85314685 45.4297631,5.85314685 C46.56407,5.85314685 47.1236614,6.83418643 47.1236614,8.09552303 C47.1236614,9.37243169 46.5489459,10.3846154 45.4297631,10.3846154 Z M51.8942607,11.2237762 C53.0952299,11.2237762 53.9728613,10.6137465 54.250008,9.7065227 L53.3723767,9.45625407 C53.141421,10.0819256 52.6063739,10.3947614 51.8942607,10.3947614 C50.8280156,10.3947614 50.0928069,9.69479135 50.0504651,8.40825421 L54.3423903,8.40825421 L54.3423903,8.01720949 C54.3423903,5.78043367 53.0336418,5.01398601 51.8018785,5.01398601 C50.2005862,5.01398601 49.1381904,6.29661271 49.1381904,8.1423438 C49.1381904,9.9880749 50.1851892,11.2237762 51.8942607,11.2237762 Z M53.5030032,7.53146853 L50.1454549,7.53146853 C50.20685,6.64403738 50.8553365,5.85314685 51.89138,5.85314685 C52.8737027,5.85314685 53.5030032,6.56606226 53.5030032,7.53146853 Z M56.7655682,11.0559441 L56.7655682,7.46804609 C56.7655682,6.42740036 57.4072575,5.83718338 58.2808828,5.83718338 C59.1274488,5.83718338 59.6415734,6.39245331 59.6415734,7.32825786 L59.6415734,11.0559441 L60.5538546,11.0559441 L60.5538546,7.26612976 C60.5538546,5.74399123 59.7459446,5.01398601 58.5437435,5.01398601 C57.6469246,5.01398601 57.0902785,5.41781869 56.8119554,6.02356769 L56.7346434,6.02356769 L56.7346434,5.09164614 L55.853287,5.09164614 L55.853287,11.0559441 L56.7655682,11.0559441 Z M66.2244746,11.0559441 L66.2244746,7.52786276 L70.4958343,7.52786276 L70.4958343,11.0559441 L71.4658866,11.0559441 L71.4658866,3.16783217 L70.4958343,3.16783217 L70.4958343,6.68050699 L66.2244746,6.68050699 L66.2244746,3.16783217 L65.2544223,3.16783217 L65.2544223,11.0559441 L66.2244746,11.0559441 Z M75.7328537,11.2237762 C76.9338229,11.2237762 77.8114543,10.6137465 78.088601,9.7065227 L77.2109697,9.45625407 C76.980014,10.0819256 76.4449669,10.3947614 75.7328537,10.3947614 C74.6666086,10.3947614 73.9313999,9.69479135 73.889058,8.40825421 L78.1809832,8.40825421 L78.1809832,8.01720949 C78.1809832,5.78043367 76.8722347,5.01398601 75.6404715,5.01398601 C74.0391792,5.01398601 72.9767834,6.29661271 72.9767834,8.1423438 C72.9767834,9.9880749 74.0237822,11.2237762 75.7328537,11.2237762 Z M77.3415962,7.53146853 L73.9840479,7.53146853 C74.045443,6.64403738 74.6939295,5.85314685 75.729973,5.85314685 C76.7122957,5.85314685 77.3415962,6.56606226 77.3415962,7.53146853 Z M81.3619048,11.2237762 C82.3877616,11.2237762 82.9236569,10.6620867 83.1073925,10.2720245 L83.1533264,10.2720245 L83.1533264,11.0833538 L84.0566928,11.0833538 L84.0566928,7.13592438 C84.0566928,5.23242085 82.6327423,5.01398601 81.8824889,5.01398601 C80.9944337,5.01398601 79.9838882,5.32603577 79.5245494,6.41820993 L80.3819819,6.73025969 C80.5810288,6.29339003 81.0518511,5.82531539 81.9131114,5.82531539 C82.7437492,5.82531539 83.1533264,6.27388692 83.1533264,7.04230945 L83.1533264,7.07351443 C83.1533264,7.51818533 82.7092988,7.47917911 81.6375081,7.6196015 C80.5465783,7.76392452 79.3561251,8.0096637 79.3561251,9.38268264 C79.3561251,10.5528692 80.2441803,11.2237762 81.3619048,11.2237762 Z M81.5864965,10.3846154 C80.8767925,10.3846154 80.3633896,10.0534566 80.3633896,9.40690853 C80.3633896,8.69728258 80.9824931,8.47651007 81.677097,8.38189327 C82.0545992,8.33458488 83.0663049,8.22419862 83.2173057,8.03496503 L83.2173057,8.88651617 C83.2173057,9.64345051 82.6435025,10.3846154 81.5864965,10.3846154 Z M86.7427314,11.0559441 L86.7427314,3.16783217 L85.9033443,3.16783217 L85.9033443,11.0559441 L86.7427314,11.0559441 Z M90.4262553,11.2237762 C90.7668998,11.2237762 90.9836735,11.1623657 91.1075442,11.1163078 L90.9217382,10.3026184 C90.844319,10.317971 90.7204482,10.3486763 90.5191583,10.3486763 C90.1165785,10.3486763 89.7294825,10.2258552 89.7294825,9.45822369 L89.7294825,6.0192344 L90.9991573,6.0192344 L90.9991573,5.25160287 L89.7294825,5.25160287 L89.7294825,3.83916084 L88.815936,3.83916084 L88.815936,5.25160287 L87.9178733,5.25Line truncated
|
||||
id="OpenHealthImagingFoundation"
|
||||
fill="#FFFFFF"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<g
|
||||
id="Group"
|
||||
transform="translate(3, 3.0579)"
|
||||
fill="#FFFFFF"
|
||||
fillRule="nonzero"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="0.25"
|
||||
>
|
||||
<path
|
||||
d="M20.874737,0 L14.2607623,0 C13.3583609,0 12.6268201,0.731540857 12.6268201,1.63394224 L12.6268201,8.24791696 C12.6268201,9.15031834 13.3583609,9.88185919 14.2607623,9.88185919 L20.874737,9.88185919 C21.7771384,9.88185919 22.5086793,9.15031834 22.5086793,8.24791696 L22.5086793,1.63394224 C22.5086793,0.731540857 21.7771384,0 20.874737,0 Z M14.2607623,0.653576894 L20.874737,0.653576894 C21.4161779,0.653576894 21.8551024,1.09250141 21.8551024,1.63394224 L21.8551024,8.24791696 C21.8551024,8.78935779 21.4161779,9.2282823 20.874737,9.2282823 L14.2607623,9.2282823 C13.7193215,9.2282823 13.280397,8.78935779 13.280397,8.24791696 L13.280397,1.63394224 C13.280397,1.09250141 13.7193215,0.653576894 14.2607623,0.653576894 Z"
|
||||
id="Rectangle"
|
||||
></path>
|
||||
<path
|
||||
d="M8.24791696,0 L1.63394224,0 C0.731540857,0 0,0.731540857 0,1.63394224 L0,8.24791696 C0,9.15031834 0.731540857,9.88185919 1.63394224,9.88185919 L8.24791696,9.88185919 C9.15031834,9.88185919 9.88185919,9.15031834 9.88185919,8.24791696 L9.88185919,1.63394224 C9.88185919,0.731540857 9.15031834,0 8.24791696,0 Z M1.63394224,0.653576894 L8.24791696,0.653576894 C8.78935779,0.653576894 9.2282823,1.09250141 9.2282823,1.63394224 L9.2282823,8.24791696 C9.2282823,8.78935779 8.78935779,9.2282823 8.24791696,9.2282823 L1.63394224,9.2282823 C1.09250141,9.2282823 0.653576894,8.78935779 0.653576894,8.24791696 L0.653576894,1.63394224 C0.653576894,1.09250141 1.09250141,0.653576894 1.63394224,0.653576894 Z"
|
||||
id="Rectangle"
|
||||
></path>
|
||||
<path
|
||||
d="M20.874737,12.6268201 L14.2607623,12.6268201 C13.3583609,12.6268201 12.6268201,13.3583609 12.6268201,14.2607623 L12.6268201,20.874737 C12.6268201,21.7771384 13.3583609,22.5086793 14.2607623,22.5086793 L20.874737,22.5086793 C21.7771384,22.5086793 22.5086793,21.7771384 22.5086793,20.874737 L22.5086793,14.2607623 C22.5086793,13.3583609 21.7771384,12.6268201 20.874737,12.6268201 Z M14.2607623,13.280397 L20.874737,13.280397 C21.4161779,13.280397 21.8551024,13.7193215 21.8551024,14.2607623 L21.8551024,20.874737 C21.8551024,21.4161779 21.4161779,21.8551024 20.874737,21.8551024 L14.2607623,21.8551024 C13.7193215,21.8551024 13.280397,21.4161779 13.280397,20.874737 L13.280397,14.2607623 C13.280397,13.7193215 13.7193215,13.280397 14.2607623,13.280397 Z"
|
||||
id="Rectangle"
|
||||
></path>
|
||||
<path
|
||||
d="M8.24791696,12.6268201 L1.63394224,12.6268201 C0.731540857,12.6268201 0,13.3583609 0,14.2607623 L0,20.874737 C0,21.7771384 0.731540857,22.5086793 1.63394224,22.5086793 L8.24791696,22.5086793 C9.15031834,22.5086793 9.88185919,21.7771384 9.88185919,20.874737 L9.88185919,14.2607623 C9.88185919,13.3583609 9.15031834,12.6268201 8.24791696,12.6268201 Z M1.63394224,13.280397 L8.24791696,13.280397 C8.78935779,13.280397 9.2282823,13.7193215 9.2282823,14.2607623 L9.2282823,20.874737 C9.2282823,21.4161779 8.78935779,21.8551024 8.24791696,21.8551024 L1.63394224,21.8551024 C1.09250141,21.8551024 0.653576894,21.4161779 0.653576894,20.874737 L0.653576894,14.2607623 C0.653576894,13.7193215 1.09250141,13.280397 1.63394224,13.280397 Z"
|
||||
id="Rectangle"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
GearSettings: (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
id="Production"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Artboard"
|
||||
transform="translate(-211, -235)"
|
||||
>
|
||||
<g
|
||||
id="icon-settings"
|
||||
transform="translate(211, 235)"
|
||||
>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
></rect>
|
||||
<path
|
||||
d="M20.2015688,10.2525279 C20.0232123,10.6274217 20.0120959,11.0603422 20.1709774,11.4438954 C20.3298588,11.8274487 20.6438515,12.1256974 21.0350638,12.2646554 L22.0250838,12.6168364 C22.6105106,12.8246232 23.00167,13.3785255 23.00167,13.9997339 C23.00167,14.6209423 22.6105106,15.1748446 22.0250838,15.3826314 L21.0350638,15.7348124 C20.6438515,15.8737704 20.3298588,16.1720191 20.1709774,16.5555724 C20.0120959,16.9391256 20.0232123,17.3720461 20.2015688,17.7469399 L20.6539257,18.6946982 C20.9219287,19.2558525 20.8071211,19.9250005 20.367394,20.3647276 C19.9276669,20.8044547 19.2585189,20.9192624 18.6973645,20.6512594 L17.748041,20.2004677 C17.3731472,20.0221111 16.9402267,20.0109948 16.5566735,20.1698763 C16.1731202,20.3287577 15.8748715,20.6427504 15.7359135,21.0339627 L15.3837325,22.0239827 C15.1756233,22.6088399 14.6220059,22.9994678 14.0012263,22.9994678 C13.3804467,22.9994678 12.8268293,22.6088399 12.6187202,22.0239827 L12.2665391,21.0339627 C12.127404,20.6426994 11.8290064,20.3287067 11.4453321,20.1698369 C11.0616578,20.0109671 10.6286351,20.0220972 10.253629,20.2004677 L9.30587073,20.6512594 C8.7446872,20.9203194 8.07479926,20.8059063 7.63473092,20.365838 C7.19466259,19.9257696 7.08024945,19.2558817 7.34930952,18.6946982 L7.80010123,17.7453747 C7.97845774,17.3704809 7.98957409,16.9375604 7.83069263,16.5540071 C7.67181118,16.1704539 7.35781846,15.8722052 6.96660615,15.7332471 L5.97658618,15.3810661 C5.39115942,15.1732793 5,14.619377 5,13.9981686 C5,13.3769603 5.39115942,12.8230579 5.97658618,12.6152712 L6.96660615,12.2630902 C7.35740035,12.124078 7.67105878,11.8260915 7.82990186,11.4429292 C7.98874494,11.0597669 7.97791757,10.6272622 7.80010123,10.2525279 L7.34930952,9.30320437 C7.08024945,8.74202085 7.19466259,8.0721329 7.63473092,7.63206456 C8.07479926,7.19199623 8.7446872,7.07758309 9.30587073,7.34664317 L10.2551942,7.79743487 C10.6298363,7.97533367 11.0622628,7.98639209 11.445508,7.82787471 C11.8287532,7.66935733 12.1270239,7.35606892 12.2665391,6.96550504 L12.6187202,5.97548507 C12.8268293,5.39062793 13.3804467,5 14.0012263,5 C14.6220059,5 15.1756233,5.39062793 15.3837325,5.97548507 L15.7359135,6.96550504 C15.8748715,7.35671735 16.1731202,7.67071008 16.5566735,7.82959153 C16.9402267,7.98847298 17.3731472,7.97735664 17.748041,7.79900012 L18.6973645,7.34664317 C19.2585189,7.07864018 19.9276669,7.19344783 20.367394,7.63317492 C20.8071211,8.07290202 20.9219287,8.74204999 20.6539257,9.30320437 L20.2015688,10.2525279 Z"
|
||||
id="Path"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<circle
|
||||
id="Oval"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
cx="14.000835"
|
||||
cy="13.9997339"
|
||||
r="3.52181017"
|
||||
></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
|
||||
// Aliases
|
||||
'tab-segmentation': (props: IconProps) => Icons.TabSegmentation(props),
|
||||
@@ -2211,4 +2860,10 @@ export const Icons = {
|
||||
'tab-patient-info': (props: IconProps) => Icons.TabPatientInfo(props),
|
||||
'tab-roi-threshold': (props: IconProps) => Icons.TabRoiThreshold(props),
|
||||
'icon-mpr': (props: IconProps) => Icons.IconMPR(props),
|
||||
'power-off': (props: IconProps) => Icons.PowerOff(props),
|
||||
'icon-multiple-patients': (props: IconProps) => Icons.MultiplePatients(props),
|
||||
'icon-patient': (props: IconProps) => Icons.Patient(props),
|
||||
'icon-chevron-patient': (props: IconProps) => Icons.ChevronPatient(props),
|
||||
info: (props: IconProps) => Icons.Info(props),
|
||||
settings: (props: IconProps) => Icons.Settings(props),
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="8.88756485px" height="5.15088921px" viewBox="0 0 8.88756485 5.15088921" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-disclosure-close</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-115.6464, -67.6464)">
|
||||
<g id="icon-disclosure-close" transform="translate(110, 60)">
|
||||
<polyline id="Path-2" stroke="#348CFD" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 737 B |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="5.15088921px" height="8.88756485px" viewBox="0 0 5.15088921 8.88756485" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>icon-disclosure-open</title>
|
||||
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Segmentation---assets" transform="translate(-152.338, -65.6013)">
|
||||
<g id="icon-disclosure-open" transform="translate(145, 60)">
|
||||
<polyline id="Path-2" stroke="#348CFD" transform="translate(10.0902, 10.0451) rotate(90) translate(-10.0902, -10.0451)" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
|
||||
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 815 B |
@@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataRow, PanelSection } from '../../index';
|
||||
import { createContext } from '../../lib/createContext';
|
||||
|
||||
interface MeasurementTableContext {
|
||||
data?: any[];
|
||||
onClick?: (uid: string) => void;
|
||||
onDelete?: (uid: string) => void;
|
||||
onToggleVisibility?: (uid: string) => void;
|
||||
onToggleLocked?: (uid: string) => void;
|
||||
onRename?: (uid: string) => void;
|
||||
onColor?: (uid: string) => void;
|
||||
disableEditing?: boolean;
|
||||
}
|
||||
|
||||
const [MeasurementTableProvider, useMeasurementTableContext] =
|
||||
createContext<MeasurementTableContext>('MeasurementTable', { data: [] });
|
||||
|
||||
interface MeasurementDataProps extends MeasurementTableContext {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const MeasurementTable = ({
|
||||
data = [],
|
||||
onClick,
|
||||
onDelete,
|
||||
onToggleVisibility,
|
||||
onToggleLocked,
|
||||
onRename,
|
||||
onColor,
|
||||
title,
|
||||
children,
|
||||
disableEditing = false,
|
||||
}: MeasurementDataProps) => {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
const amount = data.length;
|
||||
|
||||
return (
|
||||
<MeasurementTableProvider
|
||||
data={data}
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
onToggleLocked={onToggleLocked}
|
||||
onRename={onRename}
|
||||
onColor={onColor}
|
||||
disableEditing={disableEditing}
|
||||
>
|
||||
<PanelSection defaultOpen={true}>
|
||||
<PanelSection.Header className="bg-secondary-dark">
|
||||
<span>{`${t(title)} (${amount})`}</span>
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content>{children}</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</MeasurementTableProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="measurement-table-header">{children}</div>;
|
||||
};
|
||||
|
||||
const Body = () => {
|
||||
const { data } = useMeasurementTableContext('MeasurementTable.Body');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="text-primary-light mb-1 flex flex-1 items-center px-2 py-2 text-base">
|
||||
No tracked measurements
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="measurement-table-body space-y-px">
|
||||
{data.map((item, index) => (
|
||||
<Row
|
||||
key={item.uid}
|
||||
item={item}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="measurement-table-footer">{children}</div>;
|
||||
};
|
||||
|
||||
interface MeasurementItem {
|
||||
uid: string;
|
||||
label: string;
|
||||
colorHex: string;
|
||||
isSelected: boolean;
|
||||
displayText: { primary: string[]; secondary: string[] };
|
||||
isVisible: boolean;
|
||||
isLocked: boolean;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
interface RowProps {
|
||||
item: MeasurementItem;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const Row = ({ item, index }: RowProps) => {
|
||||
const {
|
||||
onClick,
|
||||
onDelete,
|
||||
onToggleVisibility,
|
||||
onToggleLocked,
|
||||
onRename,
|
||||
onColor,
|
||||
disableEditing,
|
||||
} = useMeasurementTableContext('MeasurementTable.Row');
|
||||
|
||||
return (
|
||||
<DataRow
|
||||
key={item.uid}
|
||||
description={item.label}
|
||||
number={index + 1}
|
||||
title={item.label}
|
||||
colorHex={item.colorHex}
|
||||
isSelected={item.isSelected}
|
||||
details={item.displayText}
|
||||
onSelect={() => onClick(item.uid)}
|
||||
onDelete={() => onDelete(item.uid)}
|
||||
disableEditing={disableEditing}
|
||||
isVisible={item.isVisible}
|
||||
isLocked={item.isLocked}
|
||||
onToggleVisibility={() => onToggleVisibility(item.uid)}
|
||||
onToggleLocked={() => onToggleLocked(item.uid)}
|
||||
onRename={() => onRename(item.uid)}
|
||||
// onColor={() => onColor(item.uid)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
MeasurementTable.Header = Header;
|
||||
MeasurementTable.Body = Body;
|
||||
MeasurementTable.Footer = Footer;
|
||||
MeasurementTable.Row = Row;
|
||||
|
||||
export default MeasurementTable;
|
||||
@@ -0,0 +1,3 @@
|
||||
import MeasurementTable from './MeasurementTable';
|
||||
|
||||
export { MeasurementTable };
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const stickyClasses = 'sticky top-0';
|
||||
const notStickyClasses = 'relative';
|
||||
|
||||
const NavBar = ({
|
||||
className,
|
||||
children,
|
||||
isSticky,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
isSticky?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
'bg-secondary-dark z-20 border-black px-1',
|
||||
isSticky && stickyClasses,
|
||||
!isSticky && notStickyClasses,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NavBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
isSticky: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
@@ -0,0 +1,2 @@
|
||||
import NavBar from './NavBar';
|
||||
export default NavBar;
|
||||
@@ -1,66 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '../Accordion/Accordion';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Icons } from '../Icons/Icons';
|
||||
|
||||
const PanelSection = ({ title, children, actionIcons = [], childrenClassName }) => {
|
||||
const [areChildrenVisible, setChildrenVisible] = useState(true);
|
||||
interface PanelSectionProps {
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
setChildrenVisible(!areChildrenVisible);
|
||||
};
|
||||
interface PanelSectionHeaderProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
showChevron?: boolean;
|
||||
}
|
||||
|
||||
interface PanelSectionContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PanelSection: React.FC<PanelSectionProps> & {
|
||||
Header: React.FC<PanelSectionHeaderProps>;
|
||||
Content: React.FC<PanelSectionContentProps>;
|
||||
} = ({ children, defaultOpen = true, className }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="bg-secondary-dark flex h-7 cursor-pointer select-none items-center justify-between rounded-[4px] pl-2.5 text-[13px]"
|
||||
onClick={handleHeaderClick}
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={defaultOpen ? 'item' : undefined}
|
||||
className={cn('overflow-hidden', className)}
|
||||
>
|
||||
<AccordionItem
|
||||
value="item"
|
||||
className="border-none"
|
||||
>
|
||||
<div className="text-aqua-pale">{title}</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
{actionIcons.map((icon, index) => {
|
||||
const Icon = Icons[icon.name];
|
||||
return (
|
||||
<Icon
|
||||
key={index}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!areChildrenVisible) {
|
||||
setChildrenVisible(true);
|
||||
}
|
||||
icon.onClick();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="grid h-[28px] w-[28px] place-items-center">
|
||||
{areChildrenVisible ? <Icons.ChevronOpen /> : <Icons.ChevronClosed />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{areChildrenVisible && (
|
||||
<>
|
||||
<div className="h-[2px] bg-black"></div>
|
||||
<div
|
||||
className={`bg-primary-dark flex flex-col overflow-hidden rounded-b-[4px] ${childrenClassName}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{children}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
PanelSection.propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
childrenClassName: PropTypes.string,
|
||||
actionIcons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
})
|
||||
),
|
||||
};
|
||||
PanelSection.Header = ({ children, className, showChevron = true }) => (
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
'bg-secondary-dark hover:bg-accent text-aqua-pale',
|
||||
'my-0.5 flex h-7 w-full items-center justify-between rounded py-2 pr-1 pl-2.5 text-[13px]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</AccordionTrigger>
|
||||
);
|
||||
|
||||
export { PanelSection };
|
||||
PanelSection.Header.displayName = 'PanelSection.Header';
|
||||
|
||||
PanelSection.Content = ({ children, className }) => (
|
||||
<AccordionContent className={cn('overflow-hidden p-0', className)}>
|
||||
<div className="rounded-b">{children}</div>
|
||||
</AccordionContent>
|
||||
);
|
||||
|
||||
PanelSection.Content.displayName = 'PanelSection.Content';
|
||||
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-input z-50 w-72 rounded-md border p-4 shadow-md outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -2,25 +2,101 @@ import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Icons } from '../Icons';
|
||||
|
||||
/**
|
||||
* Props interface for the ScrollArea component.
|
||||
* Extends Radix UI ScrollArea root props.
|
||||
*/
|
||||
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
||||
/** Flag to show/hide scroll indicator arrows at top and bottom */
|
||||
showArrows?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom scroll area component built on top of Radix UI's ScrollArea.
|
||||
* Provides a scrollable container with custom styling and optional scroll indicators.
|
||||
*
|
||||
* @param props - The component props
|
||||
* @param props.className - Additional CSS classes to apply
|
||||
* @param props.children - The content to be scrolled
|
||||
* @param props.showArrows - Whether to show scroll indicator arrows
|
||||
* @param ref - Forward ref for the root element
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ScrollArea showArrows>
|
||||
* <div>Scrollable content</div>
|
||||
* </ScrollArea>
|
||||
* ```
|
||||
*/
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollAreaProps
|
||||
>(({ className, children, showArrows = false, ...props }, ref) => {
|
||||
const [showBottomArrow, setShowBottomArrow] = React.useState(false);
|
||||
const [showTopArrow, setShowTopArrow] = React.useState(false);
|
||||
const viewportRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const checkScroll = React.useCallback(() => {
|
||||
if (viewportRef.current) {
|
||||
const { scrollHeight, clientHeight, scrollTop } = viewportRef.current;
|
||||
setShowBottomArrow(scrollHeight > clientHeight && scrollTop < scrollHeight - clientHeight);
|
||||
setShowTopArrow(scrollTop > 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
checkScroll();
|
||||
window.addEventListener('resize', checkScroll);
|
||||
return () => window.removeEventListener('resize', checkScroll);
|
||||
}, [checkScroll]);
|
||||
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative h-full overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={viewportRef}
|
||||
className="h-full w-full rounded-[inherit]"
|
||||
onScroll={checkScroll}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
{showArrows && showTopArrow && (
|
||||
<div className="from-background via-background/80 pointer-events-none absolute -top-1 left-0 right-0 flex h-8 items-center justify-center bg-gradient-to-b to-transparent">
|
||||
<Icons.ChevronOpen className="text-foreground/50 h-8 w-8 rotate-180" />
|
||||
</div>
|
||||
)}
|
||||
{showArrows && showBottomArrow && (
|
||||
<div className="from-background via-background/80 pointer-events-none absolute -bottom-1 left-0 right-0 flex h-8 items-center justify-center bg-gradient-to-t to-transparent">
|
||||
<Icons.ChevronOpen className="text-foreground/50 h-8 w-8" />
|
||||
</div>
|
||||
)}
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
});
|
||||
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
/**
|
||||
* Custom scrollbar component for the ScrollArea.
|
||||
* Provides styled scrollbars that can be either vertical or horizontal.
|
||||
*
|
||||
* @param props - The component props
|
||||
* @param props.className - Additional CSS classes to apply
|
||||
* @param props.orientation - The scrollbar orientation ('vertical' | 'horizontal')
|
||||
* @param ref - Forward ref for the scrollbar element
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ScrollBar orientation="vertical" />
|
||||
* ```
|
||||
*/
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
/**
|
||||
* Props interface for the AddSegmentRow component
|
||||
*/
|
||||
interface AddSegmentRowProps {
|
||||
/** Optional child elements to render within the row */
|
||||
children?: React.ReactNode;
|
||||
/** Optional segmentation object to override the active segmentation */
|
||||
segmentation?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders a row with controls for adding segments and toggling visibility
|
||||
* in the segmentation table.
|
||||
*
|
||||
* @param props - Component properties
|
||||
* @param props.children - Optional child elements to render within the row
|
||||
* @param props.segmentation - Optional segmentation object to override the active segmentation
|
||||
*/
|
||||
export const AddSegmentRow: React.FC<AddSegmentRowProps> = ({ children = null, segmentation }) => {
|
||||
const {
|
||||
activeRepresentation,
|
||||
disableEditing,
|
||||
activeSegmentationId,
|
||||
onSegmentAdd,
|
||||
onToggleSegmentationRepresentationVisibility,
|
||||
data,
|
||||
showAddSegment,
|
||||
} = useSegmentationTableContext('SegmentationTable');
|
||||
|
||||
const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every(
|
||||
segment => segment?.visible !== false
|
||||
);
|
||||
|
||||
const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId;
|
||||
|
||||
if (!data?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Icon = allSegmentsVisible ? (
|
||||
<Icons.Hide className="h-6 w-6" />
|
||||
) : (
|
||||
<Icons.Show className="h-6 w-6" />
|
||||
);
|
||||
|
||||
const allowAddSegment = showAddSegment && !disableEditing;
|
||||
|
||||
return (
|
||||
<div className="bg-primary-dark my-px flex h-7 w-full items-center justify-between rounded pl-0.5 pr-7">
|
||||
{allowAddSegment ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pr pl-0.5"
|
||||
onClick={() => onSegmentAdd(segmentationIdToUse)}
|
||||
>
|
||||
<Icons.Add />
|
||||
Add Segment
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
onToggleSegmentationRepresentationVisibility(
|
||||
segmentationIdToUse,
|
||||
activeRepresentation?.type
|
||||
)
|
||||
}
|
||||
>
|
||||
{Icon}
|
||||
</Button>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children = null,
|
||||
}) => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
|
||||
const { onSegmentationAdd, data, disableEditing, mode, disabled } =
|
||||
useSegmentationTableContext('SegmentationTable');
|
||||
|
||||
const isEmpty = data.length === 0;
|
||||
|
||||
if (!isEmpty && mode === 'collapsed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (disableEditing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group ${disabled ? 'pointer-events-none cursor-not-allowed opacity-70' : ''}`}
|
||||
onClick={() => !disabled && onSegmentationAdd('')}
|
||||
>
|
||||
{children}
|
||||
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pl-1 group-hover:cursor-pointer">
|
||||
<div className="grid h-[28px] w-[28px] place-items-center">
|
||||
{disabled ? <Icons.Info /> : <Icons.Add />}
|
||||
</div>
|
||||
<span className="text-[13px]">
|
||||
{t(`${disabled ? 'Segmentation Not Supported' : 'Add Segmentation'}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export const SegmentationCollapsed: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children = null,
|
||||
}) => {
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
import { PanelSection } from '../PanelSection';
|
||||
import { SegmentationHeader } from './SegmentationHeader';
|
||||
import { SegmentationTable } from './SegmentationTable';
|
||||
|
||||
export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { data } = useSegmentationTableContext('SegmentationExpanded');
|
||||
|
||||
// Separate the Header component from other children
|
||||
const headerComponent = React.Children.toArray(children).find(
|
||||
child => React.isValidElement(child) && child.type === SegmentationTable.Header
|
||||
);
|
||||
const otherChildren = React.Children.toArray(children).filter(
|
||||
child => !(React.isValidElement(child) && child.type === SegmentationTable.Header)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.map(segmentationInfo => (
|
||||
<PanelSection key={segmentationInfo.segmentation.segmentationId}>
|
||||
<PanelSection.Header className="border-input border-t-2 bg-transparent pl-1">
|
||||
{headerComponent ? (
|
||||
React.cloneElement(headerComponent as React.ReactElement, {
|
||||
segmentation: segmentationInfo.segmentation,
|
||||
representation: segmentationInfo.representation,
|
||||
})
|
||||
) : (
|
||||
<SegmentationHeader
|
||||
segmentation={segmentationInfo.segmentation}
|
||||
representation={segmentationInfo.representation}
|
||||
/>
|
||||
)}
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="segmentation-expanded-section">
|
||||
{React.Children.map(otherChildren, child =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child, {
|
||||
segmentation: segmentationInfo.segmentation,
|
||||
representation: segmentationInfo.representation,
|
||||
})
|
||||
: child
|
||||
)}
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { Icons } from '../Icons/Icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuPortal,
|
||||
} from '../DropdownMenu';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SegmentationHeader: React.FC<{
|
||||
segmentation?: any;
|
||||
}> = ({ segmentation }) => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
const {
|
||||
onSegmentAdd,
|
||||
onSegmentationRemoveFromViewport,
|
||||
onSegmentationEdit,
|
||||
onSegmentationDelete,
|
||||
onSegmentationDownload,
|
||||
onSegmentationDownloadRTSS,
|
||||
storeSegmentation,
|
||||
} = useSegmentationTableContext('SegmentationHeader');
|
||||
|
||||
if (!segmentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-foreground flex h-8 w-full items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-1"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Icons.More />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSegmentAdd(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Add Segment')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSegmentationRemoveFromViewport(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">{t('Remove from Viewport')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSegmentationEdit(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">{t('Rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={e => e.stopPropagation()}>
|
||||
<Icons.Hide className="text-foreground" />
|
||||
<span className="pl-2">{t('Hide or Show all Segments')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger onClick={e => e.stopPropagation()}>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Export & Download')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
storeSegmentation(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
{t('Export DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSegmentationDownload(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
{t('Download DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSegmentationDownloadRTSS(segmentation.segmentationId);
|
||||
}}
|
||||
>
|
||||
{t('Download DICOM RTSTRUCT')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onSegmentationDelete(segmentation.segmentationId)}>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">{t('Delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="pl-1.5">{segmentation.label}</div>
|
||||
</div>
|
||||
<div className="mr-1 flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.Info className="h-6 w-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{segmentation.cachedStats.info}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { ScrollArea, DataRow } from '../../components';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
export const SegmentationSegments: React.FC<{
|
||||
segmentation?: unknown;
|
||||
representation?: unknown;
|
||||
}> = ({ segmentation, representation }) => {
|
||||
const {
|
||||
activeSegmentationId,
|
||||
disableEditing,
|
||||
onSegmentColorClick,
|
||||
onToggleSegmentVisibility,
|
||||
onToggleSegmentLock,
|
||||
onSegmentClick,
|
||||
mode,
|
||||
onSegmentEdit,
|
||||
onSegmentDelete,
|
||||
data,
|
||||
} = useSegmentationTableContext('SegmentationTable.Segments');
|
||||
|
||||
let segmentationToUse = segmentation;
|
||||
let representationToUse = representation;
|
||||
let segmentationIdToUse = activeSegmentationId;
|
||||
if (!segmentationToUse || !representationToUse) {
|
||||
const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId);
|
||||
segmentationToUse = entry?.segmentation;
|
||||
representationToUse = entry?.representation;
|
||||
segmentationIdToUse = entry?.segmentation.segmentationId;
|
||||
}
|
||||
|
||||
if (!representationToUse || !segmentationToUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segmentCount = Object.keys(representationToUse.segments).length;
|
||||
const height = mode === 'collapsed' ? 'h-[600px]' : `h-[${segmentCount * 200}px]`;
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className={`ohif-scrollbar invisible-scrollbar bg-bkg-low space-y-px ${height}`}
|
||||
showArrows={true}
|
||||
>
|
||||
{Object.values(representationToUse.segments).map(segment => {
|
||||
if (!segment) {
|
||||
return null;
|
||||
}
|
||||
const { segmentIndex, color, visible } = segment;
|
||||
const segmentFromSegmentation = segmentationToUse.segments[segmentIndex];
|
||||
|
||||
if (!segmentFromSegmentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { locked, active, label, displayText } = segmentFromSegmentation;
|
||||
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||
|
||||
return (
|
||||
<DataRow
|
||||
key={segmentIndex}
|
||||
number={segmentIndex}
|
||||
title={label}
|
||||
details={displayText}
|
||||
colorHex={cssColor}
|
||||
isSelected={active}
|
||||
isVisible={visible}
|
||||
isLocked={locked}
|
||||
disableEditing={disableEditing}
|
||||
onColor={() => onSegmentColorClick(segmentationIdToUse, segmentIndex)}
|
||||
onToggleVisibility={() =>
|
||||
onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type)
|
||||
}
|
||||
onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)}
|
||||
onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)}
|
||||
onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)}
|
||||
onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Icons,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
DropdownMenuLabel,
|
||||
} from '../../components';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children = null,
|
||||
}) => {
|
||||
const { t } = useTranslation('SegmentationTable.HeaderCollapsed');
|
||||
|
||||
const {
|
||||
data,
|
||||
activeSegmentationId,
|
||||
mode,
|
||||
onSegmentationClick,
|
||||
onSegmentationAdd,
|
||||
onSegmentationRemoveFromViewport,
|
||||
onSegmentationEdit,
|
||||
onSegmentationDelete,
|
||||
onSegmentationDownload,
|
||||
onSegmentationDownloadRTSS,
|
||||
storeSegmentation,
|
||||
exportOptions,
|
||||
} = useSegmentationTableContext('SegmentationTable.HeaderCollapsed');
|
||||
|
||||
if (mode !== 'collapsed' || !data?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeSegmentationObj = data.find(
|
||||
seg => seg.segmentation.segmentationId === activeSegmentationId
|
||||
);
|
||||
|
||||
const activeSegmentation = {
|
||||
id: activeSegmentationObj?.segmentation.segmentationId,
|
||||
label: activeSegmentationObj?.segmentation.label,
|
||||
info: activeSegmentationObj?.segmentation.cachedStats?.info,
|
||||
};
|
||||
|
||||
const segmentations = data.map(seg => ({
|
||||
id: seg.segmentation.segmentationId,
|
||||
label: seg.segmentation.label,
|
||||
info: seg.segmentation.cachedStats?.info,
|
||||
}));
|
||||
|
||||
const allowExport = exportOptions.find(
|
||||
({ segmentationId }) => segmentationId === activeSegmentation.id
|
||||
)?.isExportable;
|
||||
|
||||
return (
|
||||
<div className="bg-primary-dark flex h-10 w-full items-center space-x-1 rounded-t px-1.5">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.More className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => onSegmentationAdd(activeSegmentation.id)}>
|
||||
<Icons.Add className="text-foreground" />
|
||||
<span className="pl-2">{t('Create New Segmentation')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onSegmentationRemoveFromViewport(activeSegmentation.id)}>
|
||||
<Icons.Series className="text-foreground" />
|
||||
<span className="pl-2">{t('Remove from Viewport')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSegmentationEdit(activeSegmentation.id)}>
|
||||
<Icons.Rename className="text-foreground" />
|
||||
<span className="pl-2">{t('Rename')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={!allowExport}
|
||||
className="pl-1"
|
||||
>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Export & Download')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => storeSegmentation(activeSegmentation.id)}>
|
||||
{t('Export DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSegmentationDownload(activeSegmentation.id)}>
|
||||
{t('Download DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSegmentationDownloadRTSS(activeSegmentation.id)}>
|
||||
{t('Download DICOM RTSTRUCT')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onSegmentationDelete(activeSegmentation.id)}>
|
||||
<Icons.Delete className="text-red-600" />
|
||||
<span className="pl-2 text-red-600">{t('Delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Select
|
||||
onValueChange={value => onSegmentationClick(value)}
|
||||
value={activeSegmentation.id}
|
||||
>
|
||||
<SelectTrigger className="w-full overflow-hidden">
|
||||
<SelectValue placeholder={t('Select a segmentation')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{segmentations.map(seg => (
|
||||
<SelectItem
|
||||
key={seg.id}
|
||||
value={seg.id}
|
||||
>
|
||||
{seg.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Icons.Info className="h-6 w-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
{activeSegmentation.info}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PanelSection } from '../PanelSection';
|
||||
import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext';
|
||||
import { SegmentationSegments } from './SegmentationSegments';
|
||||
import { SegmentationTableConfig } from './SegmentationTableConfig';
|
||||
import { AddSegmentRow } from './AddSegmentRow';
|
||||
import { AddSegmentationRow } from './AddSegmentationRow';
|
||||
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
|
||||
import { SegmentationHeader } from './SegmentationHeader';
|
||||
import { SegmentationCollapsed } from './SegmentationCollapsed';
|
||||
import { SegmentationExpanded } from './SegmentationExpanded';
|
||||
|
||||
interface SegmentationTableProps extends SegmentationTableContext {
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface SegmentationTableComponent extends React.FC<SegmentationTableProps> {
|
||||
Segments: typeof SegmentationSegments;
|
||||
Config: typeof SegmentationTableConfig;
|
||||
AddSegmentRow: typeof AddSegmentRow;
|
||||
AddSegmentationRow: typeof AddSegmentationRow;
|
||||
SelectorHeader: typeof SegmentationSelectorHeader;
|
||||
Header: typeof SegmentationHeader;
|
||||
Collapsed: typeof SegmentationCollapsed;
|
||||
Expanded: typeof SegmentationExpanded;
|
||||
}
|
||||
|
||||
export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => {
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props;
|
||||
|
||||
const activeSegmentationInfo = data.find(info => info.representation?.active);
|
||||
|
||||
const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId;
|
||||
const activeRepresentation = activeSegmentationInfo?.representation;
|
||||
const activeSegmentation = activeSegmentationInfo?.segmentation;
|
||||
const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } =
|
||||
activeRepresentation?.styles ?? {};
|
||||
|
||||
return (
|
||||
<SegmentationTableProvider
|
||||
data={data}
|
||||
mode={mode}
|
||||
disabled={disabled}
|
||||
disableEditing={disableEditing}
|
||||
fillAlpha={fillAlpha}
|
||||
fillAlphaInactive={fillAlphaInactive}
|
||||
outlineWidth={outlineWidth}
|
||||
renderFill={renderFill}
|
||||
renderOutline={renderOutline}
|
||||
activeSegmentationId={activeSegmentationId}
|
||||
activeSegmentation={activeSegmentation}
|
||||
activeRepresentation={activeRepresentation}
|
||||
{...contextProps}
|
||||
>
|
||||
<PanelSection defaultOpen={true}>
|
||||
<PanelSection.Header>
|
||||
<span>{t(title)}</span>
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content>{children}</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</SegmentationTableProvider>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationTable.Segments = SegmentationSegments;
|
||||
SegmentationTable.Config = SegmentationTableConfig;
|
||||
SegmentationTable.AddSegmentRow = AddSegmentRow;
|
||||
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
|
||||
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
|
||||
SegmentationTable.Header = SegmentationHeader;
|
||||
SegmentationTable.Collapsed = SegmentationCollapsed;
|
||||
SegmentationTable.Expanded = SegmentationExpanded;
|
||||
@@ -0,0 +1,177 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PanelSection } from '../PanelSection';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
|
||||
import { Slider } from '../Slider';
|
||||
import { Icons } from '../Icons';
|
||||
import { Switch } from '../Switch';
|
||||
import { Label } from '../Label';
|
||||
import { Input } from '../Input';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { t } = useTranslation('SegmentationTable.AppearanceSettings');
|
||||
const {
|
||||
renderFill,
|
||||
renderOutline,
|
||||
setRenderFill,
|
||||
setRenderOutline,
|
||||
activeRepresentation,
|
||||
fillAlpha,
|
||||
fillAlphaInactive,
|
||||
outlineWidth,
|
||||
setFillAlpha,
|
||||
setFillAlphaInactive,
|
||||
setOutlineWidth,
|
||||
renderInactiveSegmentations,
|
||||
toggleRenderInactiveSegmentations,
|
||||
data,
|
||||
} = useSegmentationTableContext('styles');
|
||||
|
||||
if (!data?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelSection defaultOpen={false}>
|
||||
<PanelSection.Header>
|
||||
<div className="flex items-center">
|
||||
<Icons.Settings className="mr-2 h-4 w-4" />
|
||||
<span>{t('Appearance Settings')}</span>
|
||||
</div>
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content>
|
||||
<div className="bg-muted mb-0.5 space-y-2 rounded-b px-1.5 pt-0.5 pb-3">
|
||||
<div className="my-1 flex items-center justify-between">
|
||||
<span className="text-aqua-pale text-xs">
|
||||
{t('Show')}:{' '}
|
||||
{renderFill && renderOutline
|
||||
? t('Fill & Outline')
|
||||
: renderOutline
|
||||
? t('Outline Only')
|
||||
: t('Fill Only')}
|
||||
</span>
|
||||
<Tabs
|
||||
value={
|
||||
renderFill && renderOutline
|
||||
? 'fill-and-outline'
|
||||
: renderOutline
|
||||
? 'outline'
|
||||
: 'fill'
|
||||
}
|
||||
onValueChange={value => {
|
||||
if (value === 'fill-and-outline') {
|
||||
setRenderFill({ type: activeRepresentation.type }, true);
|
||||
setRenderOutline({ type: activeRepresentation.type }, true);
|
||||
} else if (value === 'outline') {
|
||||
setRenderFill({ type: activeRepresentation.type }, false);
|
||||
setRenderOutline({ type: activeRepresentation.type }, true);
|
||||
} else {
|
||||
setRenderFill({ type: activeRepresentation.type }, true);
|
||||
setRenderOutline({ type: activeRepresentation.type }, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="fill-and-outline">
|
||||
<Icons.FillAndOutline className="text-primary-active" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="outline">
|
||||
<Icons.OutlineOnly className="text-primary-active" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="fill">
|
||||
<Icons.FillOnly className="text-primary-active" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
value={[fillAlpha]}
|
||||
onValueChange={([value]) =>
|
||||
setFillAlpha({ type: activeRepresentation.type }, value)
|
||||
}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
value={fillAlpha}
|
||||
onChange={e =>
|
||||
setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
|
||||
{t('Border')}
|
||||
</Label>
|
||||
<Slider
|
||||
value={[outlineWidth]}
|
||||
onValueChange={([value]) =>
|
||||
setOutlineWidth({ type: activeRepresentation.type }, value)
|
||||
}
|
||||
max={10}
|
||||
min={0}
|
||||
step={0.1}
|
||||
className="mx-1 flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={outlineWidth}
|
||||
onChange={e =>
|
||||
setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value))
|
||||
}
|
||||
className="mx-1 w-10 flex-none text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-input w-full border"></div>
|
||||
|
||||
<div className="my-2 flex items-center pl-1">
|
||||
<Switch
|
||||
checked={renderInactiveSegmentations}
|
||||
onCheckedChange={toggleRenderInactiveSegmentations}
|
||||
/>
|
||||
<Label className="text-muted-foreground mx-2 text-xs">
|
||||
{t('Display inactive segmentations')}
|
||||
</Label>
|
||||
</div>
|
||||
{renderInactiveSegmentations && (
|
||||
<div className="my-2 flex items-center">
|
||||
<Label className="text-muted-foreground w-14 flex-none whitespace-nowrap text-xs">
|
||||
Opacity
|
||||
</Label>
|
||||
<Slider
|
||||
className="mx-1 flex-1"
|
||||
value={[fillAlphaInactive]}
|
||||
onValueChange={([value]) =>
|
||||
setFillAlphaInactive({ type: activeRepresentation.type }, value)
|
||||
}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.1}
|
||||
/>
|
||||
<Input
|
||||
className="mx-1 w-10 flex-none"
|
||||
value={fillAlphaInactive}
|
||||
onChange={e =>
|
||||
setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createContext } from '../../lib/createContext';
|
||||
|
||||
interface Segmentation {
|
||||
segmentationId: string;
|
||||
label: string;
|
||||
cachedStats: {
|
||||
info: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Representation {
|
||||
active: boolean;
|
||||
visible: boolean;
|
||||
type: string;
|
||||
styles: {
|
||||
fillAlpha: number;
|
||||
fillAlphaInactive: number;
|
||||
outlineWidth: number;
|
||||
renderFill: boolean;
|
||||
renderOutline: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ViewportSegmentationInfo {
|
||||
segmentation: Segmentation;
|
||||
representation: Representation;
|
||||
}
|
||||
|
||||
interface SegmentationTableContext {
|
||||
data: ViewportSegmentationInfo[];
|
||||
disabled: boolean;
|
||||
mode: 'collapsed' | 'expanded';
|
||||
fillAlpha: number;
|
||||
exportOptions: {
|
||||
segmentationId: string;
|
||||
isExportable: boolean;
|
||||
}[];
|
||||
fillAlphaInactive: number;
|
||||
outlineWidth: number;
|
||||
renderFill: boolean;
|
||||
renderOutline: boolean;
|
||||
activeSegmentationId: string;
|
||||
activeRepresentation: Representation;
|
||||
activeSegmentation: Segmentation;
|
||||
setRenderFill: ({ type }, value: boolean) => void;
|
||||
setRenderOutline: ({ type }, value: boolean) => void;
|
||||
setOutlineWidth: ({ type }, value: number) => void;
|
||||
setFillAlpha: ({ type }, value: number) => void;
|
||||
setFillAlphaInactive: ({ type }, value: number) => void;
|
||||
renderInactiveSegmentations: boolean;
|
||||
toggleRenderInactiveSegmentations: () => void;
|
||||
onSegmentationAdd: (segmentationId: string) => void;
|
||||
onSegmentationClick: (segmentationId: string) => void;
|
||||
onSegmentationDelete: (segmentationId: string) => void;
|
||||
onSegmentAdd: (segmentationId: string) => void;
|
||||
onSegmentClick: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentEdit: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentationEdit: (segmentationId: string) => void;
|
||||
onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void;
|
||||
onSegmentDelete: (segmentationId: string, segmentIndex: number) => void;
|
||||
onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void;
|
||||
onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void;
|
||||
onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void;
|
||||
onSegmentationDownload: (segmentationId: string) => void;
|
||||
storeSegmentation: (segmentationId: string) => void;
|
||||
onSegmentationDownloadRTSS: (segmentationId: string) => void;
|
||||
setStyle: (
|
||||
segmentationId: string,
|
||||
representationType: string,
|
||||
styleKey: string,
|
||||
value: unknown
|
||||
) => void;
|
||||
onSegmentationRemoveFromViewport: (segmentationId: string) => void;
|
||||
disableEditing: boolean;
|
||||
}
|
||||
|
||||
const [SegmentationTableProvider, useSegmentationTableContext] =
|
||||
createContext<SegmentationTableContext>('SegmentationTable');
|
||||
|
||||
export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext };
|
||||
@@ -0,0 +1,22 @@
|
||||
import { SegmentationTable } from './SegmentationTable';
|
||||
import { SegmentationTableConfig } from './SegmentationTableConfig';
|
||||
import { AddSegmentationRow } from './AddSegmentationRow';
|
||||
import { AddSegmentRow } from './AddSegmentRow';
|
||||
import { SegmentationSegments } from './SegmentationSegments';
|
||||
import { SegmentationSelectorHeader } from './SegmentationSelectorHeader';
|
||||
import { SegmentationHeader } from './SegmentationHeader';
|
||||
import { useSegmentationTableContext } from './SegmentationTableContext';
|
||||
|
||||
SegmentationTable.Segments = SegmentationSegments;
|
||||
SegmentationTable.Config = SegmentationTableConfig;
|
||||
SegmentationTable.AddSegmentRow = AddSegmentRow;
|
||||
SegmentationTable.AddSegmentationRow = AddSegmentationRow;
|
||||
SegmentationTable.SelectorHeader = SegmentationSelectorHeader;
|
||||
SegmentationTable.Header = SegmentationHeader;
|
||||
|
||||
export {
|
||||
// context
|
||||
useSegmentationTableContext,
|
||||
// components
|
||||
SegmentationTable,
|
||||
};
|
||||
@@ -23,3 +23,18 @@ export {
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
||||
const SelectComponents = {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
||||
export default SelectComponents;
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
@@ -12,7 +12,7 @@ const Separator = React.forwardRef<
|
||||
SeparatorProps
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, thickness = "1px", ...props },
|
||||
{ className, orientation = 'horizontal', decorative = true, thickness = '1px', ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
@@ -20,14 +20,14 @@ const Separator = React.forwardRef<
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? `h-[${thickness}] w-full` : `h-full w-[${thickness}]`,
|
||||
'bg-border shrink-0',
|
||||
orientation === 'horizontal' ? `h-[${thickness}] w-full` : `h-full w-[${thickness}]`,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
@@ -212,43 +212,41 @@ const SidePanel = ({
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames('mt-3 flex flex-col space-y-3')}>
|
||||
<TooltipProvider>
|
||||
{_childComponents.map((childComponent, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
id={`${childComponent.name}-btn`}
|
||||
data-cy={`${childComponent.name}-btn`}
|
||||
className="text-primary-active hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
return childComponent.disabled ? null : updateActiveTabIndex(index);
|
||||
}}
|
||||
>
|
||||
{React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': childComponent.disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side === 'left' ? 'right' : 'left'}>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center',
|
||||
side === 'left' ? 'justify-end' : 'justify-start'
|
||||
)}
|
||||
>
|
||||
{getToolTipContent(childComponent.label, childComponent.disabled)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
{_childComponents.map((childComponent, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
id={`${childComponent.name}-btn`}
|
||||
data-cy={`${childComponent.name}-btn`}
|
||||
className="text-primary-active hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
return childComponent.disabled ? null : updateActiveTabIndex(index);
|
||||
}}
|
||||
>
|
||||
{React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': childComponent.disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side === 'left' ? 'right' : 'left'}>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex items-center',
|
||||
side === 'left' ? 'justify-end' : 'justify-start'
|
||||
)}
|
||||
>
|
||||
{getToolTipContent(childComponent.label, childComponent.disabled)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -296,47 +294,42 @@ const SidePanel = ({
|
||||
<div className="bg-primary-dark h-[20px] w-full"></div>
|
||||
</div>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip key={tabIndex}>
|
||||
<TooltipTrigger>
|
||||
<Tooltip key={tabIndex}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className={getTabClassNames(
|
||||
numCols,
|
||||
tabs.length,
|
||||
tabIndex,
|
||||
tabIndex === activeTabIndex,
|
||||
disabled
|
||||
)}
|
||||
style={getTabStyle(tabs.length)}
|
||||
onClick={() => {
|
||||
return disabled ? null : updateActiveTabIndex(tabIndex);
|
||||
}}
|
||||
data-cy={`${tab.name}-btn`}
|
||||
>
|
||||
<div
|
||||
className={getTabClassNames(
|
||||
numCols,
|
||||
tabs.length,
|
||||
tabIndex,
|
||||
tabIndex === activeTabIndex,
|
||||
disabled
|
||||
)}
|
||||
style={getTabStyle(tabs.length)}
|
||||
onClick={() => {
|
||||
return disabled ? null : updateActiveTabIndex(tabIndex);
|
||||
}}
|
||||
data-cy={`${tab.name}-btn`}
|
||||
className={getTabIconClassNames(tabs.length, tabIndex === activeTabIndex)}
|
||||
>
|
||||
<div
|
||||
className={getTabIconClassNames(
|
||||
tabs.length,
|
||||
tabIndex === activeTabIndex
|
||||
)}
|
||||
>
|
||||
{React.createElement(Icons[tab.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
{React.createElement(Icons[tab.iconName] || Icons.MissingIcon, {
|
||||
className: classnames({
|
||||
'text-primary-active': true,
|
||||
'ohif-disabled': disabled,
|
||||
}),
|
||||
style: {
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{getToolTipContent(tab.label, disabled)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{getToolTipContent(tab.label, disabled)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -18,16 +18,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
theme="dark"
|
||||
richColors="true"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
'group toast group-[.toaster]:bg-popover group-[.toaster]:text-foreground group-[.toaster]:border-background group-[.toaster]:shadow-lg',
|
||||
info: 'group-[.toast] group-[.toaster]:!bg-popover group-[.toaster]:!text-foreground group-[.toaster]:!border-background',
|
||||
error: 'group-[.toaster]:!border-background',
|
||||
success: 'group-[.toaster]:!border-background',
|
||||
warning: 'group-[.toaster]:!border-background',
|
||||
description: 'group-[.toast]:text-secondary-foreground',
|
||||
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
||||
style: {
|
||||
width: '430px', // Set a maximum width
|
||||
right: '8px',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -70,24 +70,24 @@ const StudyBrowser = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div data-cy={'studyBrowser-panel'}>
|
||||
{showSettings && (
|
||||
<div
|
||||
className="w-100 bg-bkg-low flex h-[48px] items-center justify-center gap-[10px] px-[8px] py-[10px]"
|
||||
data-cy={'studyBrowser-panel'}
|
||||
>
|
||||
<StudyBrowserViewOptions
|
||||
tabs={tabs}
|
||||
onSelectTab={onClickTab}
|
||||
activeTabName={activeTabName}
|
||||
/>
|
||||
<StudyBrowserSort servicesManager={servicesManager} />
|
||||
<div className="w-100 bg-bkg-low flex h-[48px] items-center justify-center gap-[10px] px-[8px] py-[10px]">
|
||||
<>
|
||||
<StudyBrowserViewOptions
|
||||
tabs={tabs}
|
||||
onSelectTab={onClickTab}
|
||||
activeTabName={activeTabName}
|
||||
/>
|
||||
<StudyBrowserSort servicesManager={servicesManager} />
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ohif-scrollbar invisible-scrollbar bg-bkg-low flex flex-1 flex-col gap-[4px] overflow-auto">
|
||||
{getTabContent()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Icons } from '../Icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../DropdownMenu/DropdownMenu';
|
||||
|
||||
export function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
||||
const { customizationService, displaySetService } = servicesManager.services;
|
||||
@@ -8,9 +14,8 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
||||
const [selectedSort, setSelectedSort] = useState(sortFunctions[0]);
|
||||
const [sortDirection, setSortDirection] = useState('ascending');
|
||||
|
||||
const handleSortChange = event => {
|
||||
const selectedSortFunction = sortFunctions.find(sort => sort.label === event.target.value);
|
||||
setSelectedSort(selectedSortFunction);
|
||||
const handleSortChange = sortFunction => {
|
||||
setSelectedSort(sortFunction);
|
||||
};
|
||||
|
||||
const toggleSortDirection = e => {
|
||||
@@ -43,26 +48,26 @@ export function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
||||
}, [displaySetService, selectedSort, sortDirection]);
|
||||
|
||||
return (
|
||||
<div className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-center rounded border bg-black p-2">
|
||||
<select
|
||||
onChange={handleSortChange}
|
||||
value={selectedSort.label}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="w-full appearance-none bg-transparent text-sm leading-tight text-white shadow transition duration-300 focus:outline-none"
|
||||
>
|
||||
{sortFunctions.map(sort => (
|
||||
<option
|
||||
className="appearance-none bg-black text-white"
|
||||
value={sort.label}
|
||||
key={sort.label}
|
||||
>
|
||||
{sort.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-start rounded border bg-black p-2 text-sm text-white">
|
||||
{selectedSort.label}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-black">
|
||||
{sortFunctions.map(sort => (
|
||||
<DropdownMenuItem
|
||||
key={sort.label}
|
||||
className="text-white"
|
||||
onClick={() => handleSortChange(sort)}
|
||||
>
|
||||
{sort.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<button
|
||||
onClick={toggleSortDirection}
|
||||
className="flex items-center justify-center"
|
||||
className="flex h-[26px] items-center justify-center bg-black"
|
||||
>
|
||||
{sortDirection === 'ascending' ? (
|
||||
<Icons.SortingAscending className="text-primary-main w-2" />
|
||||
|
||||
+21
-13
@@ -1,36 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '../DropdownMenu/DropdownMenu';
|
||||
|
||||
export function StudyBrowserViewOptions({ tabs, onSelectTab, activeTabName }: withAppTypes) {
|
||||
const handleTabChange = (tabName: string) => {
|
||||
onSelectTab(tabName);
|
||||
};
|
||||
|
||||
const activeTab = tabs.find(tab => tab.name === activeTabName);
|
||||
|
||||
return (
|
||||
<div className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-center rounded border bg-black p-2">
|
||||
<select
|
||||
onChange={e => handleTabChange(e.target.value)}
|
||||
value={activeTabName}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="w-full appearance-none bg-transparent text-sm leading-tight text-white shadow transition duration-300 focus:outline-none"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="border-inputfield-main focus:border-inputfield-main flex h-[26px] w-[125px] items-center justify-start rounded border bg-black p-2 text-sm text-white">
|
||||
{activeTab?.label}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-black">
|
||||
{tabs.map(tab => {
|
||||
const { name, label, studies } = tab;
|
||||
const isActive = activeTabName === name;
|
||||
const isDisabled = !studies.length;
|
||||
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option
|
||||
className={`appearance-none bg-black text-white ${isActive ? 'font-bold' : ''}`}
|
||||
value={name}
|
||||
<DropdownMenuItem
|
||||
key={name}
|
||||
className={`text-white ${isActive ? 'font-bold' : ''}`}
|
||||
onClick={() => handleTabChange(name)}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const StudyItem = ({
|
||||
type="single"
|
||||
collapsible
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick}
|
||||
onKeyDown={() => {}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
defaultValue={isActive ? 'study-item' : undefined}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
interface StudySummaryProps {
|
||||
date: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* StudySummary component displays a summary of a study with its date and description.
|
||||
*
|
||||
* @param props - The properties for the StudySummary component
|
||||
* @param props.date - The date of the study
|
||||
* @param props.description - The description of the study
|
||||
*/
|
||||
const StudySummary: React.FC<StudySummaryProps> = ({ date, description }) => {
|
||||
return (
|
||||
<div className="mx-2 my-0">
|
||||
<div className="text-foreground text-sm">{date}</div>
|
||||
<div className="text-muted-foreground pb-1 text-sm">{description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StudySummary };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { StudySummary } from './StudySummary';
|
||||
|
||||
export { StudySummary };
|
||||
@@ -4,7 +4,7 @@ import classnames from 'classnames';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { Icons } from '../Icons';
|
||||
import { DisplaySetMessageListTooltip } from '../DisplaySetMessageListTooltip';
|
||||
import { TooltipTrigger, TooltipContent, TooltipProvider, Tooltip } from '../Tooltip';
|
||||
import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip';
|
||||
import { Button } from '../Button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -67,7 +67,12 @@ const Thumbnail = ({
|
||||
|
||||
const renderThumbnailPreset = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-[2px] p-[4px]">
|
||||
<div
|
||||
className={classnames(
|
||||
'flex h-full w-full flex-col items-center justify-center gap-[2px] p-[4px]',
|
||||
isActive && 'bg-popover'
|
||||
)}
|
||||
>
|
||||
<div className="h-[114px] w-[128px]">
|
||||
<div className="relative">
|
||||
{imageSrc ? (
|
||||
@@ -82,7 +87,7 @@ const Thumbnail = ({
|
||||
)}
|
||||
|
||||
{/* bottom left */}
|
||||
<div className="bg-muted absolute bottom-0 left-0 flex h-[14px] items-center gap-[4px] rounded-tr pt-[10px] pb-[8px] pr-[6px] pl-[3px]">
|
||||
<div className="absolute bottom-0 left-0 flex h-[14px] items-center gap-[4px] rounded-tr pt-[10px] pb-[8px] pr-[6px] pl-[3px]">
|
||||
<div
|
||||
className={classnames(
|
||||
'h-[10px] w-[10px] rounded-[2px]',
|
||||
@@ -99,40 +104,32 @@ const Thumbnail = ({
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
{canReject && (
|
||||
<Icons.Trash
|
||||
className="h-[20px] w-[20px] text-red-500"
|
||||
onClick={onReject}
|
||||
/>
|
||||
)}
|
||||
{isTracked && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{/* bottom right */}
|
||||
@@ -148,8 +145,8 @@ const Thumbnail = ({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
asChild
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
@@ -162,12 +159,23 @@ const Thumbnail = ({
|
||||
<Icons.DicomTagBrowser />
|
||||
Tag Browser
|
||||
</DropdownMenuItem>
|
||||
{canReject && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onReject();
|
||||
}}
|
||||
className="gap-[6px]"
|
||||
>
|
||||
<Icons.Trash className="h-5 w-5 text-red-500" />
|
||||
Delete Report
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[52px] w-[128px] flex-col">
|
||||
<div className="mt-3 flex h-[52px] w-[128px] flex-col">
|
||||
<div className="min-h-[18px] w-[128px] overflow-hidden text-ellipsis pb-0.5 pl-1 text-[12px] font-normal leading-4 text-white">
|
||||
{description}
|
||||
</div>
|
||||
@@ -175,7 +183,6 @@ const Thumbnail = ({
|
||||
<div className="text-muted-foreground pl-1 text-[11px]"> S:{seriesNumber}</div>
|
||||
<div className="text-muted-foreground text-[11px]">
|
||||
<div className="flex items-center gap-[4px]">
|
||||
{' '}
|
||||
{countIcon ? (
|
||||
React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' })
|
||||
) : (
|
||||
@@ -192,7 +199,12 @@ const Thumbnail = ({
|
||||
|
||||
const renderListPreset = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-between pr-[8px] pl-[8px] pt-[4px] pb-[4px]">
|
||||
<div
|
||||
className={classnames(
|
||||
'flex h-full w-full items-center justify-between pr-[8px] pl-[8px] pt-[4px] pb-[4px]',
|
||||
isActive && 'bg-popover'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex h-[32px] items-center gap-[8px]">
|
||||
<div
|
||||
className={classnames(
|
||||
@@ -231,40 +243,33 @@ const Thumbnail = ({
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
{canReject && (
|
||||
<Icons.Trash
|
||||
className="h-[20px] w-[20px] text-red-500"
|
||||
onClick={onReject}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTracked && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="flex flex-1 flex-row">
|
||||
<div className="flex-2 flex items-center justify-center pr-4">
|
||||
<Icons.InfoLink className="text-primary-active" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span>
|
||||
<span className="text-white">
|
||||
{isTracked ? 'Series is tracked' : 'Series is untracked'}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -276,10 +281,7 @@ const Thumbnail = ({
|
||||
<Icons.More />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
asChild
|
||||
hideWhenDetached
|
||||
>
|
||||
<DropdownMenuContent hideWhenDetached>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onThumbnailContextMenu('openDICOMTagViewer', {
|
||||
@@ -291,6 +293,17 @@ const Thumbnail = ({
|
||||
<Icons.DicomTagBrowser />
|
||||
Tag Browser
|
||||
</DropdownMenuItem>
|
||||
{canReject && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onReject();
|
||||
}}
|
||||
className="gap-[6px]"
|
||||
>
|
||||
<Icons.Trash className="h-5 w-5 text-red-500" />
|
||||
Delete Report
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@ const ThumbnailList = ({
|
||||
loadingProgress,
|
||||
modality,
|
||||
componentType,
|
||||
seriesDate,
|
||||
countIcon,
|
||||
isTracked,
|
||||
canReject,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useToolbar } from '@ohif/core';
|
||||
import { ToolboxUI } from './';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { useToolbox } from '@ohif/ui';
|
||||
import { Separator } from '../Separator';
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
|
||||
@@ -110,11 +110,11 @@ function ToolboxUI(props: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
{useCollapsedPanel ? (
|
||||
<PanelSection
|
||||
childrenClassName="flex-shrink-0"
|
||||
title={title}
|
||||
>
|
||||
{render()}
|
||||
<PanelSection>
|
||||
<PanelSection.Header>
|
||||
<span>{title}</span>
|
||||
</PanelSection.Header>
|
||||
<PanelSection.Content className="flex-shrink-0">{render()}</PanelSection.Content>
|
||||
</PanelSection>
|
||||
) : (
|
||||
render()
|
||||
|
||||
@@ -50,6 +50,7 @@ import { Thumbnail } from './Thumbnail';
|
||||
import { ThumbnailList } from './ThumbnailList';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
||||
import { Toolbox, ToolboxUI } from './Toolbox';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -69,14 +70,21 @@ import {
|
||||
DropdownMenuRadioGroup,
|
||||
} from './DropdownMenu';
|
||||
import { Onboarding } from './Onboarding';
|
||||
import { BackgroundColorSelect } from './BackgroundColorSelect';
|
||||
import { DoubleSlider } from './DoubleSlider';
|
||||
import { DataRow } from './DataRow';
|
||||
import { Toaster } from './Sonner';
|
||||
import { MeasurementTable } from './MeasurementTable';
|
||||
import { SegmentationTable, useSegmentationTableContext } from './SegmentationTable';
|
||||
import { Toaster, toast } from './Sonner';
|
||||
import { StudySummary } from './StudySummary';
|
||||
import { ErrorBoundary } from './Errorboundary';
|
||||
import { Header } from './Header';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
Button,
|
||||
buttonVariants,
|
||||
ThemeWrapper,
|
||||
DoubleSlider,
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
@@ -151,12 +159,21 @@ export {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
Onboarding,
|
||||
BackgroundColorSelect,
|
||||
DataRow,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
DataRow,
|
||||
MeasurementTable,
|
||||
Toaster,
|
||||
toast,
|
||||
SegmentationTable,
|
||||
useSegmentationTableContext,
|
||||
StudySummary,
|
||||
Header,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, createContext, useContext, useCallback, useEffect } from 'react';
|
||||
import React, { createContext, useContext, useCallback, useEffect, ReactNode } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Toaster, toast } from 'sonner';
|
||||
import { Toaster, toast } from '../components';
|
||||
|
||||
const NotificationContext = createContext(null);
|
||||
|
||||
@@ -15,38 +15,41 @@ const NotificationProvider = ({ children, service }) => {
|
||||
type: 'info', // info, success, error
|
||||
};
|
||||
|
||||
const [notificationItems, setNotificationItems] = useState([]);
|
||||
|
||||
const show = useCallback(options => {
|
||||
const { title, message, duration, position, type } = { ...DEFAULT_OPTIONS, ...options };
|
||||
const id = toast[type](message, {
|
||||
const { title, message, duration, position, type, promise } = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (promise) {
|
||||
return toast.promise(promise, {
|
||||
loading: title || 'Loading...',
|
||||
success: (data: unknown) => ({
|
||||
title: title || 'Success',
|
||||
description: typeof message === 'function' ? message(data) : message,
|
||||
}),
|
||||
error: (err: unknown) => ({
|
||||
title: title || 'Error',
|
||||
description: typeof message === 'function' ? message(err) : message,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return toast[type](title, {
|
||||
duration,
|
||||
position,
|
||||
render: () => (
|
||||
<div>
|
||||
<strong>{title}</strong>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
),
|
||||
description: message,
|
||||
});
|
||||
|
||||
setNotificationItems(state => [...state, { id, ...options }]);
|
||||
}, []);
|
||||
|
||||
const hide = useCallback(id => {
|
||||
toast.dismiss(id);
|
||||
setNotificationItems(state => state.filter(item => item.id !== id));
|
||||
}, []);
|
||||
|
||||
const hideAll = useCallback(() => {
|
||||
toast.dismiss();
|
||||
setNotificationItems([]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.notification = { show, hide, hideAll };
|
||||
}, [show, hide, hideAll]);
|
||||
|
||||
/**
|
||||
* Sets the implementation of a notification service that can be used by extensions.
|
||||
*
|
||||
@@ -59,7 +62,7 @@ const NotificationProvider = ({ children, service }) => {
|
||||
}, [service, hide, show]);
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{ show, hide, hideAll, notificationItems }}>
|
||||
<NotificationContext.Provider value={{ show, hide, hideAll }}>
|
||||
<Toaster position="bottom-right" />
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BackgroundColorSelect,
|
||||
Button,
|
||||
buttonVariants,
|
||||
ThemeWrapper,
|
||||
@@ -34,7 +33,7 @@ import {
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
ScrollArea,
|
||||
DoubleSlider,
|
||||
Label,
|
||||
Slider,
|
||||
Input,
|
||||
@@ -49,7 +48,6 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
DataRow,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@@ -65,11 +63,24 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
ScrollArea,
|
||||
MeasurementTable,
|
||||
SegmentationTable,
|
||||
useSegmentationTableContext,
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
StudySummary,
|
||||
ErrorBoundary,
|
||||
Header,
|
||||
} from './components';
|
||||
import { DataRow } from './components/DataRow';
|
||||
|
||||
import { useNotification, NotificationProvider } from './contextProviders';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
// components
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -77,6 +88,7 @@ export {
|
||||
Popover,
|
||||
Combobox,
|
||||
Checkbox,
|
||||
DoubleSlider,
|
||||
buttonVariants,
|
||||
ThemeWrapper,
|
||||
Calendar,
|
||||
@@ -109,7 +121,6 @@ export {
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
ScrollArea,
|
||||
Label,
|
||||
Slider,
|
||||
Input,
|
||||
@@ -124,7 +135,6 @@ export {
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
DataRow,
|
||||
BackgroundColorSelect,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@@ -140,4 +150,14 @@ export {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
ScrollArea,
|
||||
MeasurementTable,
|
||||
SegmentationTable,
|
||||
useSegmentationTableContext,
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
StudySummary,
|
||||
Header,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// https://github.com/reach/reach-ui/blob/dev/packages/utils/src/context.tsx
|
||||
type ContextProvider<T> = React.FC<React.PropsWithChildren<T>>;
|
||||
|
||||
export function createContext<ContextValueType extends object | null>(
|
||||
rootComponentName: string,
|
||||
defaultContext?: ContextValueType
|
||||
): [ContextProvider<ContextValueType>, (callerComponentName: string) => ContextValueType] {
|
||||
const Ctx = React.createContext<ContextValueType | undefined>(defaultContext);
|
||||
|
||||
function Provider(props: React.PropsWithChildren<ContextValueType>) {
|
||||
const { children, ...context } = props;
|
||||
const value = React.useMemo(
|
||||
() => context,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
Object.values(context)
|
||||
) as ContextValueType;
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
function useContext(callerComponentName: string) {
|
||||
const context = React.useContext(Ctx);
|
||||
if (context) {
|
||||
return context;
|
||||
}
|
||||
if (defaultContext) {
|
||||
return defaultContext;
|
||||
}
|
||||
throw Error(
|
||||
`${callerComponentName} must be rendered inside of a ${rootComponentName} component.`
|
||||
);
|
||||
}
|
||||
|
||||
Ctx.displayName = `${rootComponentName}Context`;
|
||||
Provider.displayName = `${rootComponentName}Provider`;
|
||||
return [Provider, useContext];
|
||||
}
|
||||
@@ -4,6 +4,10 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* OHIF Theme */
|
||||
|
||||
@layer base {
|
||||
@@ -34,6 +38,21 @@
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--radius: 0.5rem;
|
||||
--success-bg: hsl(143, 85%, 96%);
|
||||
--success-border: hsl(145, 92%, 91%);
|
||||
--success-text: hsl(140, 100%, 27%);
|
||||
|
||||
--info-bg: hsl(208, 100%, 97%);
|
||||
--info-border: hsl(221, 91%, 91%);
|
||||
--info-text: hsl(210, 92%, 45%);
|
||||
|
||||
--warning-bg: hsl(49, 100%, 97%);
|
||||
--warning-border: hsl(49, 91%, 91%);
|
||||
--warning-text: hsl(31, 92%, 45%);
|
||||
|
||||
--error-bg: hsl(359, 100%, 97%);
|
||||
--error-border: hsl(359, 100%, 94%);
|
||||
--error-text: hsl(360, 100%, 45%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export type ContextMenuItem = {
|
||||
// A label to show for the item.
|
||||
label: string;
|
||||
// An icon to show the on right of the text - typically used for submenus
|
||||
iconRight?: string;
|
||||
// item is the menu item (eg the instance of this that is clicked on)
|
||||
// props is the remaining properties passed to the context menu
|
||||
action: (item, props) => void;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type Predicate = (props: Record<string, unknown>) => boolean;
|
||||
@@ -0,0 +1,3 @@
|
||||
type ThumbnailType = 'thumbnail' | 'thumbnailTracked' | 'thumbnailNoImage';
|
||||
|
||||
export default ThumbnailType;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface ViewportActionCornersComponentInfo {
|
||||
id: string;
|
||||
component: ReactNode;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ThumbnailType from './ThumbnailType';
|
||||
|
||||
// A few miscellaneous types declared inline here.
|
||||
|
||||
export * from './Predicate';
|
||||
export * from './ContextMenuItem';
|
||||
export * from './ViewportActionCornersTypes';
|
||||
|
||||
/**
|
||||
* StringNumber often comes back from DICOMweb for integer valued items.
|
||||
*/
|
||||
type StringNumber = string | number;
|
||||
|
||||
/**
|
||||
* StringArray often comes back from dcmjs for single valued strings that
|
||||
* might have multiple values such as window level descriptions.
|
||||
*/
|
||||
type StringArray = string | string[];
|
||||
|
||||
export type { StringNumber, StringArray, ThumbnailType };
|
||||
Reference in new issue
Block a user