ui(components): migrates to new toolbar ui components and Numeric for Input InputRange and InputDoubleRange (#4728)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
1 parent
ac5ae4a29e
commit
a8d84443a9
116 files changed
+2930
-1038
No files matched your search
@@ -38,12 +38,12 @@ export interface ButtonProps
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
({ className, variant, size, asChild = false, ...props }, forwardRef) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
ref={forwardRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,10 +11,22 @@ interface DoubleSliderProps {
|
||||
step?: number;
|
||||
defaultValue?: [number, number];
|
||||
onValueChange?: (value: [number, number]) => void;
|
||||
showNumberInputs?: boolean;
|
||||
}
|
||||
|
||||
const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
({ className, min, max, step = 1, defaultValue = [min, max], onValueChange }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
min,
|
||||
max,
|
||||
onValueChange,
|
||||
step = 1,
|
||||
defaultValue = [min, max],
|
||||
showNumberInputs = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [value, setValue] = React.useState<[number, number]>(defaultValue);
|
||||
|
||||
const prevDefaultValueRef = React.useRef<[number, number] | null>(null);
|
||||
@@ -77,16 +89,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
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}
|
||||
/>
|
||||
{showNumberInputs && (
|
||||
<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}
|
||||
@@ -101,16 +115,18 @@ const DoubleSlider = React.forwardRef<HTMLDivElement, DoubleSliderProps>(
|
||||
<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}
|
||||
/>
|
||||
{showNumberInputs && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -689,6 +689,9 @@ export const Icons = {
|
||||
'icon-status-alert': (props: IconProps) => Alert(props),
|
||||
'info-link': (props: IconProps) => InfoLink(props),
|
||||
'launch-info': (props: IconProps) => LaunchInfo(props),
|
||||
'old-trash': (props: IconProps) => Trash(props),
|
||||
'tool-point': (props: IconProps) => ToolCircle(props),
|
||||
'tool-freehand-line': (props: IconProps) => ToolFreehand(props),
|
||||
clipboard: (props: IconProps) => Clipboard(props),
|
||||
|
||||
/** Adds an icon to the set of icons */
|
||||
|
||||
@@ -2447,8 +2447,6 @@ export const ToolSegBrush = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -2490,8 +2488,6 @@ export const ToolSegEraser = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -2592,8 +2588,6 @@ export const ToolSegThreshold = (props: IconProps) => (
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -3012,10 +3006,11 @@ export const ToolWindowLevel = (props: IconProps) => (
|
||||
|
||||
export const ToolWindowRegion = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
@@ -3029,8 +3024,8 @@ export const ToolWindowRegion = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M10.3460449,22 L4,22 C3.44771525,22 3,21.5522847 3,21 L3,4 C3,3.44771525 3.44771525,3 4,3 L21,3 C21.5522847,3 22,3.44771525 22,4 L22,11.3197021"
|
||||
@@ -3110,11 +3105,9 @@ export const ToolZoom = (props: IconProps) => (
|
||||
|
||||
export const ToolBrush = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -3128,8 +3121,8 @@ export const ToolBrush = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M3.24640621,21.8286833 C3.09173375,21.7551472 2.99513748,21.5971513 3.00018895,21.4259625 C3.00524042,21.2547737 3.11098486,21.1027485 3.26972426,21.0384606 C5.3260304,20.2059201 4.66362518,17.8620247 5.27421252,16.6088957 C6.02197747,15.1026514 7.84114758,14.4766383 9.35746132,15.2037675 C13.9485253,17.4422999 8.48346644,24.3211232 3.24640621,21.8286833 Z"
|
||||
@@ -3153,11 +3146,9 @@ export const ToolBrush = (props: IconProps) => (
|
||||
|
||||
export const ToolEraser = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -3171,8 +3162,8 @@ export const ToolEraser = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<path
|
||||
d="M8.47826087,18.0030772 C7.48731947,17.9495317 6.54439458,17.5591084 5.80565217,16.8964685 L4.1066087,15.1958598 C3.49763625,14.5834856 3.49763625,13.5942339 4.1066087,12.9818598 L13.6317391,3.45672934 C14.2441133,2.84775689 15.233365,2.84775689 15.8457391,3.45672934 L19.8926087,7.5035989 C20.5015811,8.11597307 20.5015811,9.10522473 19.8926087,9.7175989 L12.7153043,16.8949033 C11.9769855,17.5579476 11.0343414,17.9489213 10.0434783,18.0030772 L8.47826087,18.0030772 Z"
|
||||
@@ -3210,11 +3201,9 @@ export const ToolEraser = (props: IconProps) => (
|
||||
|
||||
export const ToolThreshold = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -3228,8 +3217,8 @@ export const ToolThreshold = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<g
|
||||
id="Group"
|
||||
@@ -3295,11 +3284,9 @@ export const ToolThreshold = (props: IconProps) => (
|
||||
|
||||
export const ToolShape = (props: IconProps) => (
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 28 28"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
@@ -3313,8 +3300,8 @@ export const ToolShape = (props: IconProps) => (
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="28"
|
||||
height="28"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<circle
|
||||
id="Oval"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Icons } from './Icons';
|
||||
|
||||
export { Icons };
|
||||
export default Icons;
|
||||
@@ -0,0 +1,277 @@
|
||||
// Numeric.tsx
|
||||
import React, { createContext, useContext, useState, useCallback, PropsWithChildren } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Slider } from '../Slider/Slider';
|
||||
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
|
||||
|
||||
interface NumericMetaContextValue {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
singleValue: number;
|
||||
doubleValue: [number, number];
|
||||
setSingleValue: (val: number) => void;
|
||||
setDoubleValue: (vals: [number, number]) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
1) Container
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumericMetaContainerProps {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
value?: number; // for single-value usage
|
||||
values?: [number, number]; // for double-range usage
|
||||
onChange?: (val: number | [number, number]) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function NumericMetaContainer({
|
||||
mode,
|
||||
value = 0,
|
||||
values = [0, 100],
|
||||
onChange,
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<NumericMetaContainerProps>) {
|
||||
// Initialize state with props but don't update automatically
|
||||
const [internalSingleValue, setInternalSingleValue] = useState<number>(value);
|
||||
const [internalDoubleValue, setInternalDoubleValue] = useState<[number, number]>(values);
|
||||
|
||||
const handleSingleChange = useCallback(
|
||||
(newVal: number) => {
|
||||
setInternalSingleValue(newVal);
|
||||
onChange?.(newVal);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleDoubleChange = useCallback(
|
||||
(newVals: [number, number]) => {
|
||||
// Update internal state
|
||||
setInternalDoubleValue(newVals);
|
||||
// Notify parent if onChange is provided
|
||||
onChange?.(newVals);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<NumericMetaContext.Provider
|
||||
value={{
|
||||
mode,
|
||||
singleValue: internalSingleValue,
|
||||
doubleValue: internalDoubleValue,
|
||||
setSingleValue: handleSingleChange,
|
||||
setDoubleValue: handleDoubleChange,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex flex-col', className)}>{children}</div>
|
||||
</NumericMetaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
2) Label sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumericMetaLabelProps {
|
||||
showValue?: boolean; // optionally show the current numeric value(s)
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumericMetaLabel must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, doubleValue } = ctx;
|
||||
|
||||
let displayedValue = '';
|
||||
let valueClasses = '';
|
||||
if (mode === 'number' || mode === 'singleRange') {
|
||||
displayedValue = singleValue.toString();
|
||||
valueClasses = 'w-10';
|
||||
} else if (mode === 'doubleRange') {
|
||||
displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('text-foreground flex text-base', className)}>
|
||||
{children}
|
||||
{showValue && (
|
||||
<span className={cn('inline-block', valueClasses)}>{`: ${displayedValue}`}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
3) SingleRange sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
|
||||
interface SingleRangeProps {
|
||||
showNumberInput?: boolean;
|
||||
sliderClassName?: string;
|
||||
numberInputClassName?: string;
|
||||
}
|
||||
|
||||
function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('SingleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(val: number[]) => {
|
||||
setSingleValue(val[0]);
|
||||
},
|
||||
[setSingleValue]
|
||||
);
|
||||
|
||||
const handleNumberChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const parsed = parseFloat(evt.target.value);
|
||||
if (!isNaN(parsed)) {
|
||||
setSingleValue(Math.max(min, Math.min(parsed, max)));
|
||||
}
|
||||
},
|
||||
[min, max, setSingleValue]
|
||||
);
|
||||
|
||||
if (mode !== 'singleRange') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<Slider
|
||||
className={cn('flex-1', sliderClassName)}
|
||||
value={[singleValue]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onValueChange={handleSliderChange}
|
||||
/>
|
||||
{showNumberInput && (
|
||||
<Input
|
||||
type="number"
|
||||
className={cn('w-[50px] shrink-0', numberInputClassName)}
|
||||
value={singleValue}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={handleNumberChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
4) DoubleRange sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface DoubleRangeProps {
|
||||
showNumberInputs?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('DoubleRange must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, doubleValue, setDoubleValue, min, max, step } = ctx;
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
(values: [number, number]) => {
|
||||
setDoubleValue(values);
|
||||
},
|
||||
[setDoubleValue]
|
||||
);
|
||||
|
||||
if (mode !== 'doubleRange') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1', className)}>
|
||||
<DoubleSlider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={doubleValue}
|
||||
onValueChange={handleSliderChange}
|
||||
showNumberInputs={showNumberInputs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
5) Basic NumberInput sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumberInputProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function NumberInput({ className }: NumberInputProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumberInput must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
|
||||
if (mode !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseFloat(evt.target.value);
|
||||
if (!isNaN(val)) {
|
||||
setSingleValue(Math.max(min, Math.min(val, max)));
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate width based on max value's length, with a minimum of 3 characters
|
||||
const maxLength = Math.max(3, max?.toString().length ?? 3);
|
||||
const calculatedWidth = `${maxLength + 1.5}ch`;
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={singleValue}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={handleChange}
|
||||
className={cn('min-w-[60px]', `w-[${calculatedWidth}]`, className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const Numeric = {
|
||||
Container: NumericMetaContainer,
|
||||
Label: NumericMetaLabel,
|
||||
SingleRange,
|
||||
DoubleRange,
|
||||
NumberInput,
|
||||
};
|
||||
|
||||
export default Numeric;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Numeric from './Numeric';
|
||||
|
||||
export default Numeric;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import Numeric from '../Numeric';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RowDoubleRangeProps {
|
||||
values: [number, number];
|
||||
onChange: (values: [number, number]) => void;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
step: number;
|
||||
showLabel?: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RowDoubleRange: React.FC<RowDoubleRangeProps> = ({
|
||||
values,
|
||||
onChange,
|
||||
minValue,
|
||||
maxValue,
|
||||
step,
|
||||
showLabel = false,
|
||||
label = '',
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={step}
|
||||
className={cn('flex flex-col space-y-2', className)}
|
||||
>
|
||||
{showLabel && <Numeric.Label showValue>{label}</Numeric.Label>}
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowDoubleRange;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import Numeric from '../Numeric';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RowInputRangeProps {
|
||||
value: number;
|
||||
onChange: (newValue: number) => void;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
labelPosition?: 'left' | 'right';
|
||||
allowNumberEdit?: boolean;
|
||||
showNumberInput?: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const RowInputRange: React.FC<RowInputRangeProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
step = 1,
|
||||
label = '',
|
||||
showLabel = false,
|
||||
labelPosition = 'right',
|
||||
allowNumberEdit = false,
|
||||
showNumberInput = true,
|
||||
className,
|
||||
containerClassName,
|
||||
}) => {
|
||||
const handleChange = (newValue: number | [number, number]) => {
|
||||
if (typeof newValue === 'number') {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
onChange(newValue[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Numeric.Container
|
||||
mode="singleRange"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={step}
|
||||
className={cn('flex flex-row items-center space-x-2', className)}
|
||||
>
|
||||
{showLabel && label && labelPosition === 'left' && (
|
||||
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
|
||||
)}
|
||||
<Numeric.SingleRange showNumberInput={allowNumberEdit && showNumberInput} />
|
||||
{showLabel && label && labelPosition === 'right' && (
|
||||
<Numeric.Label showValue={showNumberInput}>{label}</Numeric.Label>
|
||||
)}
|
||||
</Numeric.Container>
|
||||
);
|
||||
|
||||
return containerClassName ? <div className={containerClassName}>{content}</div> : content;
|
||||
};
|
||||
|
||||
export default RowInputRange;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Label } from '../Label';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../Tabs';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface RadioValue {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioOption {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
values: RadioValue[];
|
||||
commands?: (val: string) => void;
|
||||
}
|
||||
|
||||
interface RowSegmentedControlProps {
|
||||
option: RadioOption;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RowSegmentedControl: React.FC<RowSegmentedControlProps> = ({ option, className }) => {
|
||||
const handleValueChange = (newVal: string) => {
|
||||
if (option.commands) {
|
||||
option.commands(newVal);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex items-center justify-between text-[13px]', className)}
|
||||
key={option.id}
|
||||
>
|
||||
<Label className="mr-2">{option.name}</Label>
|
||||
<div className="max-w-1/2">
|
||||
<Tabs
|
||||
value={option.value}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<TabsList className="inline-flex space-x-1">
|
||||
{option.values.map(({ label, value: itemValue }, index) => (
|
||||
<TabsTrigger
|
||||
value={itemValue}
|
||||
key={`button-${option.id}-${index}`}
|
||||
>
|
||||
{label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowSegmentedControl;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import RowInputRange from './RowInputRange';
|
||||
import RowSegmentedControl from './RowSegmentedControl';
|
||||
import RowDoubleRange from './RowDoubleRange';
|
||||
|
||||
const SETTING_TYPES = {
|
||||
RANGE: 'range',
|
||||
RADIO: 'radio',
|
||||
CUSTOM: 'custom',
|
||||
DOUBLE_RANGE: 'double-range',
|
||||
};
|
||||
|
||||
function ToolSettings({ options }) {
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
return options();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-foreground space-y-2 pb-4">
|
||||
{options?.map(option => {
|
||||
if (option.condition && option.condition?.({ options }) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case SETTING_TYPES.RANGE:
|
||||
return renderRangeSetting(option);
|
||||
case SETTING_TYPES.RADIO:
|
||||
return renderRadioSetting(option);
|
||||
case SETTING_TYPES.DOUBLE_RANGE:
|
||||
return renderDoubleRangeSetting(option);
|
||||
case SETTING_TYPES.CUSTOM:
|
||||
return renderCustomSetting(option);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderRangeSetting = option => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={option.id}
|
||||
>
|
||||
<div className="w-1/3 text-[13px]">{option.name}</div>
|
||||
<div className="w-2/3">
|
||||
<RowInputRange
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
value={option.value}
|
||||
onChange={value => option.commands?.(value)}
|
||||
allowNumberEdit={true}
|
||||
inputClassName="ml-1 w-4/5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRadioSetting = option => {
|
||||
return (
|
||||
<RowSegmentedControl
|
||||
key={option.id}
|
||||
option={option}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function renderDoubleRangeSetting(option) {
|
||||
return (
|
||||
<RowDoubleRange
|
||||
key={option.id}
|
||||
values={option.value}
|
||||
onChange={option.commands}
|
||||
minValue={option.min}
|
||||
maxValue={option.max}
|
||||
step={option.step}
|
||||
showLabel={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderCustomSetting = option => {
|
||||
return (
|
||||
<div key={option.id}>
|
||||
{typeof option.children === 'function' ? option.children() : option.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSettings;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ToolSettings } from './ToolSettings';
|
||||
+15
-14
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { ToolboxUI, useToolbox } from '../../';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
import { ToolboxUI } from './';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { useToolbox } from '@ohif/ui';
|
||||
|
||||
/**
|
||||
* A toolbox is a collection of buttons and commands that they invoke, used to provide
|
||||
@@ -21,6 +19,8 @@ function Toolbox({
|
||||
title,
|
||||
...props
|
||||
}: withAppTypes) {
|
||||
// We should move these outside of the platform/ui-next, no file here
|
||||
// should rely on the managers and services
|
||||
const { state: toolboxState, api } = useToolbox(buttonSectionId);
|
||||
const { onInteraction, toolbarButtons } = useToolbar({
|
||||
servicesManager,
|
||||
@@ -117,6 +117,9 @@ function Toolbox({
|
||||
|
||||
if (items?.length) {
|
||||
items.forEach(({ options, id }) => {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
accumulator[id] = createEnhancedOptions(options, id);
|
||||
});
|
||||
} else if (options?.length) {
|
||||
@@ -144,17 +147,15 @@ function Toolbox({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
</>
|
||||
<ToolboxUI
|
||||
{...props}
|
||||
title={title}
|
||||
toolbarButtons={toolbarButtons}
|
||||
toolboxState={toolboxState}
|
||||
handleToolSelect={id => api.handleToolSelect(id)}
|
||||
handleToolOptionChange={handleToolOptionChange}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+13
-17
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { PanelSection } from '../../components';
|
||||
// Migrate this file to the new UI eventually
|
||||
import { ToolSettings } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { PanelSection } from '../../components';
|
||||
import { ToolSettings } from '../OHIFToolSettings';
|
||||
|
||||
const ItemsPerRow = 4;
|
||||
|
||||
function usePrevious(value) {
|
||||
@@ -55,8 +55,8 @@ function ToolboxUI(props: withAppTypes) {
|
||||
const render = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-black">
|
||||
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-muted mt-0.5 flex flex-wrap space-x-2 py-2 px-1">
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
@@ -81,20 +81,16 @@ function ToolboxUI(props: withAppTypes) {
|
||||
key={id}
|
||||
className={classnames({
|
||||
[toolClasses]: true,
|
||||
'border-secondary-light flex flex-col items-center justify-center rounded-md border':
|
||||
true,
|
||||
})}
|
||||
>
|
||||
<div className="flex rounded-md bg-black">
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
{...componentProps}
|
||||
{...props}
|
||||
id={id}
|
||||
servicesManager={servicesManager}
|
||||
onInteraction={onInteraction}
|
||||
size="toolbox"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
File renamed without changes.
@@ -101,7 +101,7 @@ const Thumbnail = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.StatusTracking className="text-primary-light h-[15px] w-[15px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
@@ -216,7 +216,7 @@ const Thumbnail = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="group">
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[20px] group-hover:hidden" />
|
||||
<Icons.StatusTracking className="text-primary-light h-[20px] w-[15px] group-hover:hidden" />
|
||||
<Icons.Cancel
|
||||
className="text-primary-light hidden h-[15px] w-[15px] group-hover:block"
|
||||
onClick={onClickUntrack}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
|
||||
import { Icons } from '../Icons';
|
||||
import { Button } from '../Button';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const baseClasses = '!rounded-lg inline-flex items-center justify-center';
|
||||
const defaultClasses = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight';
|
||||
const activeClasses = 'bg-highlight text-background hover:!bg-highlight/80';
|
||||
const disabledClasses =
|
||||
'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed';
|
||||
|
||||
const sizeClasses = {
|
||||
default: {
|
||||
buttonSizeClass: 'w-10 h-10',
|
||||
iconSizeClass: 'h-7 w-7',
|
||||
},
|
||||
small: {
|
||||
buttonSizeClass: 'w-8 h-8',
|
||||
iconSizeClass: 'h-6 w-6',
|
||||
},
|
||||
};
|
||||
|
||||
interface ToolButtonProps {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
size?: 'default' | 'small';
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledText?: string;
|
||||
commands?: Record<string, unknown>;
|
||||
onInteraction?: (details: { itemId: string; commands?: Record<string, unknown> }) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ToolButton(props: ToolButtonProps) {
|
||||
const {
|
||||
id,
|
||||
icon = 'MissingIcon',
|
||||
label,
|
||||
tooltip,
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
isActive = false,
|
||||
disabledText,
|
||||
commands,
|
||||
onInteraction,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const { buttonSizeClass, iconSizeClass } = sizeClasses[size];
|
||||
|
||||
const buttonClasses = cn(
|
||||
baseClasses,
|
||||
buttonSizeClass,
|
||||
disabled ? disabledClasses : isActive ? activeClasses : defaultClasses,
|
||||
className
|
||||
);
|
||||
|
||||
const defaultTooltip = tooltip || label;
|
||||
const disabledTooltip = disabled && disabledText ? disabledText : null;
|
||||
const hasTooltip = defaultTooltip || disabledTooltip;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
className={cn(disabled && 'cursor-not-allowed')}
|
||||
>
|
||||
{/* TooltipTrigger is a span since a disabled button does not fire events and the tooltip
|
||||
will not show. */}
|
||||
<span
|
||||
data-cy={id}
|
||||
data-tool={id}
|
||||
data-active={isActive}
|
||||
>
|
||||
<Button
|
||||
className={buttonClasses}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onInteraction?.({ itemId: id, commands });
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={hasTooltip ? defaultTooltip : undefined}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icons.ByName
|
||||
name={icon}
|
||||
className={iconSizeClass}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{hasTooltip && (
|
||||
<>
|
||||
<div>{defaultTooltip}</div>
|
||||
{disabledTooltip && <div className="text-muted-foreground">{disabledTooltip}</div>}
|
||||
</>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolButton;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { Icons } from '../Icons';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../DropdownMenu';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip';
|
||||
|
||||
/**
|
||||
* ToolButtonList Component
|
||||
* Root component that wraps the default and dropdown sections
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ToolButtonList = React.forwardRef<HTMLDivElement, ToolButtonListProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
ToolButtonList.displayName = 'ToolButtonList';
|
||||
|
||||
/**
|
||||
* ToolButtonListDefault Component
|
||||
* Container for the default/primary tool button
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListDefaultProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
tooltip?: string;
|
||||
disabledText?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ToolButtonListDefault = React.forwardRef<HTMLDivElement, ToolButtonListDefaultProps>(
|
||||
({ className, children, tooltip, disabledText, disabled, ...props }, ref) => {
|
||||
const hasTooltip = tooltip || disabledText;
|
||||
|
||||
const defaultContent = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!hasTooltip) {
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{defaultContent}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{tooltip && <div>{tooltip}</div>}
|
||||
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
ToolButtonListDefault.displayName = 'ToolButtonListDefault';
|
||||
|
||||
/**
|
||||
* ToolButtonListDropDown Component
|
||||
* Container for the dropdown section with trigger and content
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListDropDownProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ToolButtonListDropDown = React.forwardRef<HTMLDivElement, ToolButtonListDropDownProps>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<DropdownMenu {...props}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'text-foreground/80 hover:bg-background hover:text-highlight border-primary',
|
||||
'inline-flex h-10 w-5 items-center justify-center',
|
||||
'!rounded-tr-lg !rounded-br-lg !rounded-tl-none !rounded-bl-none',
|
||||
'bg-transparent',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icons.ByName
|
||||
name="chevron-down"
|
||||
className="text-primary h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
ref={ref}
|
||||
side="bottom"
|
||||
align="start"
|
||||
alignOffset={-40}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
ToolButtonListDropDown.displayName = 'ToolButtonListDropDown';
|
||||
|
||||
/**
|
||||
* ToolButtonListItem Component
|
||||
* Individual item in the dropdown menu
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
interface ToolButtonListItemProps extends React.ComponentProps<typeof DropdownMenuItem> {
|
||||
icon?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
disabledText?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
const ToolButtonListItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuItem>,
|
||||
ToolButtonListItemProps
|
||||
>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => {
|
||||
const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined);
|
||||
const hasTooltip = defaultTooltip || disabledText;
|
||||
|
||||
const menuItem = (
|
||||
<DropdownMenuItem
|
||||
ref={ref}
|
||||
className={cn('flex items-center space-x-2', className)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{icon && (
|
||||
<Icons.ByName
|
||||
name={icon || 'MissingIcon'}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
// Todo: there is a weird issue where i can't control the duration of the delay
|
||||
// for the items in this list, causing the tooltip to show up too early in the
|
||||
// dropdown menu. So i'm just removing the tooltip for list items unless the disabledText is set.
|
||||
if (!disabled) {
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{menuItem}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{defaultTooltip && <div>{defaultTooltip}</div>}
|
||||
{disabledText && disabled && <div className="text-muted-foreground">{disabledText}</div>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
ToolButtonListItem.displayName = 'ToolButtonListItem';
|
||||
|
||||
/**
|
||||
* ToolButtonListDivider Component
|
||||
* Divider between items in the dropdown menu
|
||||
* -----------------------------------------------
|
||||
*/
|
||||
const ToolButtonListDivider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('bg-primary h-5 w-px self-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToolButtonListDivider.displayName = 'ToolButtonListDivider';
|
||||
|
||||
export {
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as ToolButton } from './ToolButton';
|
||||
export {
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButtonList';
|
||||
@@ -52,7 +52,9 @@ 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 { ToolboxUI, Toolbox } from './OHIFToolbox';
|
||||
import Numeric from './Numeric';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -91,8 +93,17 @@ import {
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
} from './Viewport';
|
||||
import {
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
} from './ToolButton';
|
||||
|
||||
export {
|
||||
Numeric,
|
||||
ErrorBoundary,
|
||||
Button,
|
||||
buttonVariants,
|
||||
@@ -157,8 +168,8 @@ export {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
Toolbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
@@ -208,4 +219,10 @@ export {
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
Clipboard,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
export const initialState = {};
|
||||
|
||||
export const toolboxReducer = (state, action) => {
|
||||
const { toolbarSectionId } = action.payload;
|
||||
|
||||
if (!state[toolbarSectionId]) {
|
||||
state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false };
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_TOOL':
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
activeTool: action.payload.activeTool,
|
||||
selectedEvent: true,
|
||||
},
|
||||
};
|
||||
case 'UPDATE_TOOL_OPTION':
|
||||
const { toolName, optionName, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[toolbarSectionId]: {
|
||||
...state[toolbarSectionId],
|
||||
selectedEvent: false,
|
||||
toolOptions: {
|
||||
...state[toolbarSectionId].toolOptions,
|
||||
[toolName]: state[toolbarSectionId].toolOptions[toolName].map(option =>
|
||||
option.id === optionName ? { ...option, value } : option
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'INITIALIZE_TOOL_OPTIONS':
|
||||
// Initialize tool options for each toolbarSectionId
|
||||
return {
|
||||
...state,
|
||||
selectedEvent: false,
|
||||
[action.toolbarSectionId]: {
|
||||
...state[action.toolbarSectionId],
|
||||
toolOptions: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const ToolboxContext = createContext();
|
||||
|
||||
export const ToolboxProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const handleToolSelect = (toolbarSectionId, toolName) => {
|
||||
dispatch({
|
||||
type: 'SET_ACTIVE_TOOL',
|
||||
payload: { toolbarSectionId, activeTool: toolName },
|
||||
});
|
||||
};
|
||||
|
||||
const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_TOOL_OPTION',
|
||||
payload: { toolbarSectionId, toolName, optionName, value: newValue },
|
||||
});
|
||||
};
|
||||
|
||||
const initializeToolOptions = (toolbarSectionId, toolOptions) => {
|
||||
dispatch({
|
||||
type: 'INITIALIZE_TOOL_OPTIONS',
|
||||
toolbarSectionId,
|
||||
payload: toolOptions,
|
||||
});
|
||||
};
|
||||
|
||||
const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions };
|
||||
|
||||
const value = { state, api };
|
||||
|
||||
return <ToolboxContext.Provider value={value}>{children}</ToolboxContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for accessing toolbox state and actions for a specific toolbar section.
|
||||
* You can use this hook to access the state and actions for a specific toolbar section (
|
||||
* defined by the toolbarSectionId) in your custom toolbar components. This hook
|
||||
* helps to manage the state and actions for the tools and their options in the toolbar.
|
||||
*/
|
||||
export const useToolbox = toolbarSectionId => {
|
||||
const context = useContext(ToolboxContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useToolbox must be used within a ToolboxProvider');
|
||||
}
|
||||
const { state, api } = context;
|
||||
|
||||
return {
|
||||
state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} },
|
||||
api: {
|
||||
handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName),
|
||||
handleToolOptionChange: (toolName, optionName, value) =>
|
||||
api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value),
|
||||
initializeToolOptions: toolOptions =>
|
||||
api.initializeToolOptions(toolbarSectionId, toolOptions),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import NotificationProvider, { useNotification } from './NotificationProvider';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
|
||||
import { ToolboxProvider, useToolbox } from './ToolboxContext';
|
||||
|
||||
export { useNotification, NotificationProvider };
|
||||
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
|
||||
export { ToolboxProvider, useToolbox };
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
DoubleSlider,
|
||||
Label,
|
||||
@@ -93,11 +92,24 @@ import {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
Toolbox,
|
||||
} from './components';
|
||||
import { DataRow } from './components/DataRow';
|
||||
|
||||
import { useNotification, NotificationProvider } from './contextProviders';
|
||||
import {
|
||||
useNotification,
|
||||
NotificationProvider,
|
||||
useToolbox,
|
||||
ToolboxProvider,
|
||||
} from './contextProviders';
|
||||
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
|
||||
import * as utils from './utils';
|
||||
|
||||
export {
|
||||
ErrorBoundary,
|
||||
@@ -143,7 +155,6 @@ export {
|
||||
ThumbnailList,
|
||||
PanelSection,
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
Label,
|
||||
Slider,
|
||||
@@ -202,4 +213,14 @@ export {
|
||||
ViewportActionCornersLocations,
|
||||
ViewportOverlay,
|
||||
ViewportGrid,
|
||||
ToolButton,
|
||||
ToolButtonList,
|
||||
ToolButtonListDefault,
|
||||
ToolButtonListDropDown,
|
||||
ToolButtonListItem,
|
||||
ToolButtonListDivider,
|
||||
ToolboxProvider,
|
||||
Toolbox,
|
||||
useToolbox,
|
||||
utils,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
const getToggledClassName = isToggled => {
|
||||
return isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
|
||||
};
|
||||
|
||||
export { getToggledClassName };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { getToggledClassName } from './getToggledClassName';
|
||||
|
||||
export { getToggledClassName };
|
||||
Reference in new issue
Block a user