improvement: treatment plan turned into a wizard. shift+click control added to FDI tooth chart for cunnected prosthesises.

This commit is contained in:
2026-07-17 00:39:32 +03:30
parent cf07b4d8a8
commit 68fc9d5d6d
28 changed files with 1461 additions and 477 deletions

View File

@@ -21,7 +21,9 @@ function quadrantMirrored(fdi: FdiToothId): boolean {
interface FdiToothChartProps {
selected: ReadonlySet<FdiToothId>;
onToggle?: (fdi: FdiToothId) => void;
/** Teeth that belong to a connected selection span (dots above/below). */
connectedTeeth?: ReadonlySet<FdiToothId>;
onToggle?: (fdi: FdiToothId, event: { shiftKey: boolean }) => void;
disabled?: boolean;
/** Non-interactive display (Cases / connection history). */
readOnly?: boolean;
@@ -38,6 +40,7 @@ interface FdiToothChartProps {
export function FdiToothChart({
selected,
connectedTeeth,
onToggle,
disabled,
readOnly = false,
@@ -147,6 +150,7 @@ export function FdiToothChart({
const kind = getToothShapeKind(fdi);
const gid = `${uid}-g-${fdi}-${i}`;
const isSel = selected.has(fdi);
const isConnected = Boolean(connectedTeeth?.has(fdi));
const size = toothSizeClass(fdi);
const tweak = TOOTH_TWEAKS[fdi];
const offsetY = tweak ? tweak.offset : archOffset(i, teeth.length, upper);
@@ -168,24 +172,44 @@ export function FdiToothChart({
/>
);
const connectedDot = isConnected ? (
<span
className={`block h-1.5 w-1.5 rounded-full bg-primary ${upper ? 'mb-0.5' : 'mt-0.5'}`}
aria-hidden
title={t('toothConnectedHint')}
/>
) : (
<span className={`block h-1.5 w-1.5 ${upper ? 'mb-0.5' : 'mt-0.5'}`} aria-hidden />
);
return (
<div key={fdi} className={`flex flex-col items-center ${size.wrapper}`}>
{upper ? connectedDot : null}
<div className={`h-[5.9rem] flex ${alignItems} justify-center`}>
{interactive ? (
<button
type="button"
disabled={isDisabled}
onClick={() => onToggle?.(fdi)}
onMouseDown={(e) => {
// Shift+click otherwise triggers browser text-selection / sticky focus boxes.
if (e.shiftKey) e.preventDefault();
}}
onClick={(e) => {
e.preventDefault();
onToggle?.(fdi, { shiftKey: e.shiftKey });
}}
style={{ transform: `translateY(${offsetY}px) rotate(${rotate}deg)` }}
className={`
rounded-[var(--radius-sm)] p-0.5 transition-transform
rounded-[var(--radius-sm)] p-0.5 transition-transform select-none
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105 active:scale-95'}
`}
aria-pressed={isSel}
aria-label={
isSel
? `${t('toothAria', { fdi })}${t('toothSelectedSuffix')}`
? `${t('toothAria', { fdi })}${t('toothSelectedSuffix')}${
isConnected ? t('toothConnectedSuffix') : ''
}`
: t('toothAria', { fdi })
}
>
@@ -201,6 +225,7 @@ export function FdiToothChart({
</div>
)}
</div>
{!upper ? connectedDot : null}
</div>
);
})}
@@ -215,8 +240,8 @@ export function FdiToothChart({
<>
<p className="text-[11px] uppercase tracking-wide text-text-muted mb-1 text-center">{t('upperArch')}</p>
<div className="overflow-x-auto py-1 -mx-1 px-1">
<div className="relative isolate min-w-max mx-auto w-fit">
<div className="overflow-x-auto py-1 -mx-1 px-1 select-none">
<div className="relative isolate min-w-max mx-auto w-fit select-none">
<div
className="pointer-events-none absolute left-1/2 top-3 bottom-3 w-px -translate-x-1/2 bg-border/70"
aria-hidden