Skip to content

Commit dd1bc2b

Browse files
committed
fix: selection issue, reduced file size
1 parent 744e6dc commit dd1bc2b

10 files changed

Lines changed: 1121 additions & 120 deletions

File tree

biome.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"enabled": true
2121
}
2222
},
23-
2423
"linter": {
2524
"enabled": true,
2625
"rules": {
@@ -63,7 +62,7 @@
6362
"correctness": {
6463
"useImportExtensions": "off",
6564
"noUndeclaredDependencies": "off",
66-
"noUnusedVariables": "off",
65+
"noUnusedVariables": "info",
6766
"noNodejsModules": "off"
6867
}
6968
}

lefthook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pre-commit:
22
parallel: true
33
commands:
44
lint:
5-
run: pnpm biome check --write --unsafe --staged --no-errors-on-unmatched && git add -u
5+
run: git add -u
66
typecheck:
77
run: pnpm tsc
88
build:

src/Odontogram.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import "./styles.css";
2+
import { teethPaths } from "./data";
3+
import { useCallback, useState } from "react";
4+
5+
interface TeethProps {
6+
name: string;
7+
outlinePath: string;
8+
shadowPath: string;
9+
lineHighlightPath: string | string[];
10+
selected?: boolean;
11+
onClick?: (name: string) => void;
12+
onKeyDown?: (e: React.KeyboardEvent<SVGGElement>, name: string) => void;
13+
}
14+
15+
const Teeth = ({
16+
name,
17+
outlinePath,
18+
shadowPath,
19+
lineHighlightPath,
20+
selected,
21+
onClick,
22+
onKeyDown,
23+
}: TeethProps) => (
24+
<g
25+
className={`${name} ${selected ? "selected" : ""}`}
26+
tabIndex={0}
27+
onClick={() => onClick?.(name)}
28+
onKeyDown={(e) => onKeyDown?.(e, name)}
29+
role="button"
30+
aria-pressed={selected}
31+
aria-label={`Tooth ${name}`}
32+
style={{
33+
cursor: "pointer",
34+
outline: "none",
35+
touchAction: "manipulation",
36+
transition: "all 0.2s ease",
37+
}}
38+
>
39+
<title>{name}</title>
40+
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" d={outlinePath} />
41+
<path fill="currentColor" d={shadowPath} />
42+
{Array.isArray(lineHighlightPath)
43+
? lineHighlightPath.map((d, i) => (
44+
<path key={i} stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" d={d} />
45+
))
46+
: <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" d={lineHighlightPath} />}
47+
</g>
48+
);
49+
50+
export interface OdontogramProps {
51+
defaultSelected?: string[];
52+
onChange?: (selected: string[]) => void;
53+
className?: string;
54+
selectedColor?: string;
55+
hoverColor?: string;
56+
}
57+
58+
const Odontogram: React.FC<OdontogramProps> = ({
59+
defaultSelected = [],
60+
onChange,
61+
className = "",
62+
selectedColor = "#1E90FF",
63+
hoverColor = "#60A5FA",
64+
}) => {
65+
const [selected, setSelected] = useState<Set<string>>(new Set(defaultSelected));
66+
67+
const handleToggle = useCallback(
68+
(name: string) => {
69+
setSelected((prev) => {
70+
const updated = new Set(prev);
71+
updated.has(name) ? updated.delete(name) : updated.add(name);
72+
onChange?.(Array.from(updated));
73+
return updated;
74+
});
75+
},
76+
[onChange]
77+
);
78+
79+
const handleKeyDown = useCallback(
80+
(e: React.KeyboardEvent<SVGGElement>, name: string) => {
81+
if (e.key === "Enter" || e.key === " ") {
82+
e.preventDefault();
83+
handleToggle(name);
84+
}
85+
},
86+
[handleToggle]
87+
);
88+
89+
const quadrants = [
90+
{ name: "first", transform: "" },
91+
{ name: "second", transform: "scale(-1, 1) translate(-409, 0)" },
92+
{ name: "third", transform: "scale(1, -1) translate(0, -694)" },
93+
{ name: "fourth", transform: "scale(-1, -1) translate(-409, -694)" },
94+
];
95+
96+
const renderTeeth = (prefix: string) =>
97+
teethPaths.map((tooth) => {
98+
const id = `${prefix}${tooth.name}`;
99+
return (
100+
<Teeth
101+
key={id}
102+
{...tooth}
103+
name={id}
104+
selected={selected.has(id)}
105+
onClick={handleToggle}
106+
onKeyDown={handleKeyDown}
107+
/>
108+
);
109+
});
110+
111+
return (
112+
<div
113+
className={`OdontogramWrapper ${className}`}
114+
style={{
115+
width: "100%",
116+
maxWidth: 300,
117+
margin: "0 auto",
118+
display: "flex",
119+
justifyContent: "center",
120+
alignItems: "center",
121+
}}
122+
>
123+
<svg
124+
xmlns="http://www.w3.org/2000/svg"
125+
fill="none"
126+
viewBox="0 0 409 694"
127+
className="Odontogram"
128+
style={{
129+
width: "100%",
130+
height: "auto",
131+
userSelect: "none",
132+
touchAction: "manipulation",
133+
}}
134+
>
135+
136+
137+
<g name="upper">
138+
{quadrants.slice(0, 2).map(({ name, transform }, index) => (
139+
<g key={name} name={name} transform={transform}>
140+
{renderTeeth(`teeth-${index + 1}`)}
141+
</g>
142+
))}
143+
</g>
144+
145+
<g name="lower">
146+
{quadrants.slice(2).map(({ name, transform }, index) => (
147+
<g key={name} name={name} transform={transform}>
148+
{renderTeeth(`teeth-${index + 3}`)}
149+
</g>
150+
))}
151+
</g>
152+
</svg>
153+
</div>
154+
);
155+
};
156+
157+
export default Odontogram;

src/data.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export const teethPaths = [
2+
{
3+
name: "1",
4+
outlinePath: "M173.951 1.698c-2.198.16-5.302.862-5.302.862s-5.531 1.025-7.781 2.27c-2.403 1.333-5.553 3.299-6.013 5.258-.432 1.84-.78 4.734-.491 6.832l.027.2c.348 2.527.865 6.28 1.239 10.316.186 2-.101 3.21.751 6.16.895 3.1 1.395 4.573 2.013 5.687.646 1.164 1.493 2.087 2.03 3.28.866 1.924 2.393 4.29 3.87 6.1a48.805 48.805 0 0 0 2.36 2.663c1.016 1.078 2.819 3.11 3.898 4.331 1.526 1.727 3.076 3.248 4.423 3.742 1.217.446 2.709.642 3.793.936 1.369.372 2.494.638 4.597 0 1.545-.468 2.725-.621 4.372-2.424 2.064-2.258 2.846-3.517 3.512-4.746 1.065-1.964 2.524-4.648 3.541-6.422.611-1.068 2.221-3.089 4.086-8.845 1.54-4.752 2.367-7.76 2.731-8.994.415-1.408 1.079-3.583 1.355-6.196.44-4.176 1.114-7.482 1.062-11.026-.044-3.046.104-5.04-1.065-6.306-.95-1.029-1.988-2.001-3.306-2.534-1.959-.79-3.775-1.358-5.298-1.592-1.351-.207-7.05-.35-11.219-.057-1.567.11-3.61.097-9.185.505Z",
5+
shadowPath: "M188.269 50.215c19.275-41.867 10.766-43.612 4.937-44.684-5.829-1.073-28.455 2.53-29.72 2.817-.442.218-8.911 0-4.767 19.54 3.416 16.105 16.193 26.596 17.244 27.265 3.148 2.003 8.837 2.595 12.306-4.938Z",
6+
lineHighlightPath: "M160.472 14.727c.392-.402 4.317-3.309 8.941-5.47 3.097-1.448 7.773-1.629 11.207-1.749 8.759-.365 10.004.91 12.637 1.495.562.139.909.276 1.771.595",
7+
},
8+
9+
{
10+
name: "2",
11+
outlinePath: "M135.016 60.41c3.443 1.355 7.834 2.73 9.601 2.346 4.185-.91 7.044-4.017 8.123-8.152 2.069-7.927 2.249-16.288 1.995-24.436-.113-3.596.72-16.81-4.184-18.444-8.135-6.14-32.088.494-36.979 8.055-5.013 7.751.878 21.131 5.56 27.728 3.862 5.44 9.714 10.474 15.884 12.903Z",
12+
shadowPath: "M134.65 55.386c2.732 1.076 6.217 2.167 7.62 1.862 3.321-.723 5.59-3.188 6.446-6.47 1.642-6.29 1.786-12.927 1.583-19.393-.089-2.854.572-13.342-3.32-14.638-6.456-4.873-25.466.392-29.348 6.393-3.979 6.152.696 16.77 4.413 22.006 3.064 4.317 7.709 8.313 12.606 10.24Z",
13+
lineHighlightPath: "M121.569 28.952c.551-.984 6.354-8.214 12.866-9.045 3.238-.413 10.619-.17 11.838 0",
14+
},
15+
{
16+
name: "3",
17+
outlinePath: "M77.63 64.236c1.692 1.401 15.516 14.583 34.698 16.903 5.569.674 15.058-7.184 15.789-12.89 1.335-10.423-12.444-29.205-21.965-33.703-4.542-2.145-25.892-4.039-28.523 2.939-2.471 6.557-6.596 21.29 0 26.751Z",
18+
shadowPath: "M123.285 66.267c1.072-8.368-10.234-23.94-17.99-27.603-3.72-1.757-21.206-3.308-23.36 2.407-1.757 4.66-4.09 13.284-2.306 18.17 4.15 11.36 40.897 28.573 43.656 7.026Z",
19+
lineHighlightPath: ["M82.84 60.043c-.01-.855-.193-4.63.256-8.12.667-5.185.703-5.866 1.29-8.106.437-1.666 2.083-4.91 17.487-3.748", "M101.438 68.49c.376-.045 2.284-5.112 9.498-6.344"]
20+
},
21+
{
22+
name: "4",
23+
outlinePath: "M93.684 108.748c-7.498 3.397-35.368-7.397-35.596-7.441-1.429-.842-12.215-2.854-9.068-29.414.147-1.238 1.353-2.05 2.218-2.556.956-.559 3.157-1.595 7.587-2.794 2.707-.734 13.856-3.54 18.674-.92 2.216 1.206 6.363 4.117 10.473 7.036 4.11 2.919 10.958 8.715 13.327 11.338 2.291 2.537 10.324 13.75-7.615 24.751Z",
24+
shadowPath: "M91.495 105.931c-6.528 2.958-30.793-6.44-30.992-6.478-1.243-.734-10.634-2.486-7.895-25.61.128-1.078 1.178-1.785 1.932-2.225.832-.487 2.748-1.39 6.605-2.433 2.357-.639 12.064-3.082 16.258-.8 1.93 1.05 5.54 3.584 9.12 6.125 3.578 2.541 9.54 7.588 11.603 9.872 1.994 2.208 8.987 11.971-6.63 21.549Z",
25+
lineHighlightPath: ["M55.234 92.414c.022-.242.06-1.469.875-7.79.46-3.573 1.813-5.49 3.134-7.01 1.18-1.359 3.524-3.193 5.88-4.365 1.46-.632 3.029-1.126 6.044-1.776.826-.138 1.477-.175 2.526-.395", "M87.02 101.387c4.183-.792 8.747-9.774 8.747-9.774"]
26+
},
27+
{
28+
name: "5",
29+
outlinePath: "M33.396 127.652c-1.088-2.713-4.486-11.852-3.962-15.246.477-3.085 14.47-12.008 23.595-12.008 2.178 0 25.157 6.665 35.073 17.908 4.551 9.324-1.624 23.339-16.22 23.339-8.193 0-23.563-1.897-25.453-2.215-1.896-.319-4.317-1.128-6.596-2.321-1.289-.675-2-1.035-2.478-1.647-.655-.84-2.101-3.178-3.96-7.81Z",
30+
shadowPath: "M36.837 126.778c-.945-2.356-3.895-10.289-3.44-13.235.414-2.679 12.561-10.424 20.483-10.424 1.89 0 21.837 5.785 30.445 15.545 3.951 8.094-1.41 20.26-14.08 20.26-7.112 0-20.454-1.647-22.095-1.922-1.645-.277-3.747-.98-5.725-2.015-1.12-.586-1.737-.899-2.151-1.43-.57-.729-1.825-2.759-3.437-6.779Z",
31+
lineHighlightPath: ['M40.8 131.221c-4.003-20.278 8.442-20.974 13.779-23.692', 'M74.056 134.729c6.35-4.264 6.343-10.503 6.489-11.448']
32+
},
33+
{
34+
name: "6",
35+
outlinePath: "M35.539 203.323c-3.363.196-32.788-4.02-28.495-42.465 3.631-20.408 26.21-23.364 29.557-22.912 9.019-.745 25.727 6.743 29.255 9.403 4.546 3.427 7.384 5.681 8.606 7.092 2.436 2.814 4.499 5.11 4.7 6.313.284 1.703 1.317 4.72-2.248 9.687-2.453 3.418-2.957 6.663-2.856 10.223.118 4.185.37 9.754-.524 11.63-.592 1.244-.984 2.627-2.375 3.392-2.015 1.108-4.61 2.317-11.93 3.748-3.623.708-9.309 1.83-12.401 2.292-5.19.777-8.423 1.43-11.289 1.597Z",
36+
shadowPath: "M36.515 199.085c-2.927.17-28.54-3.499-24.803-36.963 3.16-17.764 22.814-20.337 25.727-19.944 10.82-.893 25.709 5.989 32.955 14.358 2.12 2.45 3.917 4.448 4.091 5.495.248 1.482.513 4.268-2.59 8.591-2.135 2.975-1.94 5.641-1.852 8.74.096 3.417 1.402 10.917-2.523 13.076-1.754.964-4.013 2.016-10.384 3.261-6.762 1.322-13.728 2.984-20.621 3.386Z",
37+
lineHighlightPath: ["M37.132 192.147c1.288-.372 3.385-2.071 5.183-5.438m9.008-35.98c1.188 1.224 2.114 6.385.15 11.363m0 0c-1.537 3.899-4.944 9.378-11.195 9.378 6.877.321 4.651 10.343 2.037 15.239m9.159-24.617c1.47-2.589 4.966-7.572 8.068-8.217m-17.227 32.834c-.186 2.348.044 7.351 2.458 8.584"]
38+
},
39+
{
40+
name: "7",
41+
outlinePath: "M4.969 248.132c-1.188-2.74-14.708-27.152 12.319-42.139 1.836-1.018 24.508-7.711 45.28 10.874 2.274 2.035 4.455 3.579 5.403 5.102 1.414 2.271 2.128 3.435 1.687 5.606-.387 1.901-1.54 3.642-3.77 7.711-1.127 2.056-1.854 3.427-1.369 7.824.175 1.59.627 3.935.048 6.603-.466 2.147-.941 3.567-2.578 5.006-2.53 2.224-5.307 3.645-9.076 4.35-1.091.204-3.133.811-7.525 1.702-2.776.563-6.619 1.234-10.09 1.654-3.47.42-6.478.638-8.612.636-3.353-.002-6.734.031-9.465-1.48l-.018-.01c-1.654-.915-4.076-2.255-6.245-4.732-2.258-2.579-4.591-5.482-5.989-8.707Z",
42+
shadowPath: "M8.799 246.28c-1.128-2.352-12.843-23.709 10.756-36.796 1.604-.889 21.4-6.732 39.538 9.496 1.986 1.777 3.89 3.125 4.717 4.454 1.235 1.983 1.859 3 1.473 4.895-.337 1.66-1.343 3.18-3.291 6.734-.984 1.795-1.619 2.992-1.196 6.831.154 1.389.548 3.436.042 5.766-2.872 13.231-31.213 11.657-33.076 11.656h-.007c-2.936-.003-12.714-.01-18.956-13.036Z",
43+
lineHighlightPath: ["M32.14 255.091c.748-.303 2.28-2.378 3.103-5.721m-1.99-35.481c3.009 1.363 4.015 4.888 3.736 8.716m0 0c-.424 5.835-3.832 12.377-7.683 13.049 6.954 1.18 7.244 8.414 5.937 13.716m1.746-26.765c.327-2.24 1.82-6.958 5.19-7.91m-6.936 34.675c.65 2.172 2.55 6.507 4.967 6.471"]
44+
},
45+
{
46+
name: "8",
47+
outlinePath: "M.895 291.331c-.362-1.218-.116-3.411-.164-3.892-.035-.34.124-3.438.727-5.558 1.04-2.82 1.808-4.74 2.573-5.752 1.413-1.872 3.913-4.769 7.135-7.2 2.288-1.726 5.003-3.187 8.133-4.339 2.249-.828 4.362-1.502 7.732-1.693 2.175-.123 5.25-.155 8.782.108s7.403.804 10.353 1.397 4.92 1.218 6.641 1.893c1.722.675 3.148 1.389 3.972 1.768 1.51.696 2.9 1.209 3.824 2.14 1.857 1.872 2.617 3.334 2.912 4.807.254 1.27.252 3.216-.224 4.531-.672 1.859-1.468 2.856-1.468 5.7 0 3.857 1.713 5.95 1.915 8.356.337 4.008.317 6.168-.42 7.485-.591 1.057-1.532 2.383-5.541 5.088-1.173.791-2.684 1.634-6.643 3.424-2.762 1.248-6.977 3.108-9.205 4.107-2.473 1.109-4.667 2.226-8.234 3.053-2.54.589-5.631.784-9.012.186-2.307-.408-5.449-1.183-9.107-3.827-3.928-2.839-6.53-4.897-7.497-6.041-.866-1.026-1.714-2.082-2.412-3.555-.904-1.905-2.272-3.643-3-5.916-.846-2.633-1.167-4.233-1.772-6.27Z",
48+
shadowPath: "M4.732 291.175c-.317-1.07-.101-2.994-.144-3.416-.03-.299.11-3.019.638-4.88.914-2.476 1.588-4.161 2.26-5.05 1.24-1.643 3.434-4.187 6.263-6.321 2.009-1.515 4.393-2.797 7.14-3.809 1.974-.727 3.83-1.318 6.788-1.486 1.91-.108 4.61-.136 7.71.095 3.1.231 6.498.706 9.088 1.226 2.59.521 4.319 1.07 5.83 1.662 1.512.592 2.764 1.22 3.487 1.553 1.326.61 2.547 1.061 3.357 1.878 1.63 1.643 2.298 2.927 2.557 4.22.223 1.115.221 2.824-.196 3.978-.59 1.632-1.29 2.507-1.29 5.004 0 3.385 1.504 5.223 1.682 7.335.296 3.519.278 5.416-.369 6.571-.52.928-1.345 2.093-4.865 4.467-1.03.695-2.355 1.435-5.831 3.006-2.425 1.096-6.126 2.728-8.082 3.606-2.17.973-4.096 1.954-7.228 2.68-2.229.517-4.944.688-7.912.163-2.024-.358-4.783-1.038-7.995-3.359-3.448-2.493-5.732-4.299-6.58-5.304-.761-.901-1.505-1.827-2.119-3.121-.793-1.672-1.994-3.198-2.634-5.193-.742-2.312-1.024-3.717-1.555-5.505Z",
49+
lineHighlightPath: ["M31.732 309.511c1.462-.897 2.585-3.672 2.93-6.726m-2.93-29.453c2.114 1.092 3.49 4.44 3.61 8.062m4.133-7.58c-1.353 1.058-4.074 4.057-4.134 7.58m0 0c.155 4.693-1.797 9.846-6.983 11.152 5.379-.357 6.859 5.305 6.303 10.239m4.557 7.24c-1.678-.671-4.938-3.058-4.557-7.24"]
50+
}
51+
]

0 commit comments

Comments
 (0)