@salloomd/teeth-selector 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -0
- package/dist/index.js +1875 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# @salloomd/teeth-selector
|
|
2
|
+
|
|
3
|
+
A headless React component for dental tooth selection with bridge support. Follows the [shadcn/ui](https://ui.shadcn.com/) code style and patterns.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @salloomd/teeth-selector
|
|
9
|
+
# or
|
|
10
|
+
npm install @salloomd/teeth-selector
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🎨 **Headless** - Bring your own styles
|
|
16
|
+
- 🦷 **Complete dental chart** - All 32 teeth with FDI numbering
|
|
17
|
+
- 🌉 **Bridge support** - Select dental bridges between adjacent teeth
|
|
18
|
+
- 📱 **Touch support** - Drag selection works on mobile
|
|
19
|
+
- 🎯 **Fully typed** - Written in TypeScript
|
|
20
|
+
- âš¡ **Render props** - Full control over rendering
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Basic Example (with Tailwind CSS)
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { TeethSelector, TeethChart } from "@salloomd/teeth-selector";
|
|
28
|
+
|
|
29
|
+
function MyDentalForm() {
|
|
30
|
+
return (
|
|
31
|
+
<TeethSelector
|
|
32
|
+
onChange={(selectedTeeth, bridges) => {
|
|
33
|
+
console.log("Selected:", selectedTeeth);
|
|
34
|
+
console.log("Bridges:", bridges);
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{({ selectedTeeth, clearAll, selectUpper, selectLower }) => (
|
|
38
|
+
<div className="space-y-4">
|
|
39
|
+
<div className="flex gap-2">
|
|
40
|
+
<button onClick={selectUpper}>Select Upper</button>
|
|
41
|
+
<button onClick={selectLower}>Select Lower</button>
|
|
42
|
+
{selectedTeeth.length > 0 && (
|
|
43
|
+
<button onClick={clearAll}>Clear All</button>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<TeethChart
|
|
48
|
+
width={300}
|
|
49
|
+
height={500}
|
|
50
|
+
renderTooth={({ tooth, isSelected, isInDragRange }, defaultEl) => (
|
|
51
|
+
<path
|
|
52
|
+
key={tooth.id}
|
|
53
|
+
id={`teeth-${tooth.id}`}
|
|
54
|
+
d={tooth.d}
|
|
55
|
+
className={`
|
|
56
|
+
cursor-pointer transition-colors
|
|
57
|
+
${isSelected ? "fill-amber-400 stroke-gray-600" : "fill-white stroke-gray-400"}
|
|
58
|
+
${isInDragRange ? "fill-blue-300" : ""}
|
|
59
|
+
hover:fill-amber-200
|
|
60
|
+
`}
|
|
61
|
+
onClick={defaultEl.props.onClick}
|
|
62
|
+
onMouseDown={defaultEl.props.onMouseDown}
|
|
63
|
+
onMouseEnter={defaultEl.props.onMouseEnter}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
renderBridge={({ fromId, toId, exists, position }, defaultEl) => (
|
|
67
|
+
<g key={`${fromId}-${toId}`}>
|
|
68
|
+
{exists && (
|
|
69
|
+
<line
|
|
70
|
+
x1={position.x1}
|
|
71
|
+
y1={position.y1}
|
|
72
|
+
x2={position.x2}
|
|
73
|
+
y2={position.y2}
|
|
74
|
+
stroke="#ff6b6b"
|
|
75
|
+
strokeWidth={4}
|
|
76
|
+
strokeDasharray="5,3"
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
<circle
|
|
80
|
+
cx={position.midX}
|
|
81
|
+
cy={position.midY}
|
|
82
|
+
r={8}
|
|
83
|
+
className={`
|
|
84
|
+
cursor-pointer transition-all
|
|
85
|
+
${
|
|
86
|
+
exists
|
|
87
|
+
? "fill-blue-500 stroke-white stroke-2"
|
|
88
|
+
: "fill-gray-200 stroke-gray-400 opacity-0 hover:opacity-100"
|
|
89
|
+
}
|
|
90
|
+
`}
|
|
91
|
+
onClick={defaultEl.props.children[1].props.onClick}
|
|
92
|
+
/>
|
|
93
|
+
</g>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</TeethSelector>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### TeethPreview (Read-only)
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { TeethPreview } from "@salloomd/teeth-selector";
|
|
107
|
+
|
|
108
|
+
function Preview() {
|
|
109
|
+
// String format: individual teeth or bridged ranges
|
|
110
|
+
// "11, 21, [31, 33]" means teeth 11, 21, and bridged 31-32-33
|
|
111
|
+
return (
|
|
112
|
+
<TeethPreview
|
|
113
|
+
teeth="11, 21, [31, 33]"
|
|
114
|
+
width={150}
|
|
115
|
+
height={250}
|
|
116
|
+
renderTooth={({ tooth, isSelected }, defaultEl) => (
|
|
117
|
+
<path
|
|
118
|
+
key={tooth.id}
|
|
119
|
+
id={`teeth-preview-${tooth.id}`}
|
|
120
|
+
d={tooth.d}
|
|
121
|
+
className={
|
|
122
|
+
isSelected
|
|
123
|
+
? "fill-amber-400 stroke-gray-600"
|
|
124
|
+
: "fill-white stroke-gray-400"
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Controlled Mode
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useState } from "react";
|
|
137
|
+
import {
|
|
138
|
+
TeethSelector,
|
|
139
|
+
TeethChart,
|
|
140
|
+
type Tooth,
|
|
141
|
+
} from "@salloomd/teeth-selector";
|
|
142
|
+
|
|
143
|
+
function ControlledExample() {
|
|
144
|
+
const [selectedTeeth, setSelectedTeeth] = useState<number[]>([11, 21]);
|
|
145
|
+
const [bridges, setBridges] = useState<number[][]>([[11, 21]]);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<TeethSelector
|
|
149
|
+
selectedTeeth={selectedTeeth}
|
|
150
|
+
bridges={bridges}
|
|
151
|
+
onSelectionChange={(teeth: Tooth[]) => {
|
|
152
|
+
setSelectedTeeth(teeth.map((t) => t.id));
|
|
153
|
+
}}
|
|
154
|
+
onBridgeChange={setBridges}
|
|
155
|
+
>
|
|
156
|
+
{(props) => (
|
|
157
|
+
<TeethChart
|
|
158
|
+
renderTooth={({ tooth, isSelected }, defaultEl) => (
|
|
159
|
+
<path
|
|
160
|
+
key={tooth.id}
|
|
161
|
+
id={`teeth-${tooth.id}`}
|
|
162
|
+
d={tooth.d}
|
|
163
|
+
style={{
|
|
164
|
+
fill: isSelected ? "#fbbf24" : "#fff",
|
|
165
|
+
stroke: isSelected ? "#4b5563" : "#9ca3af",
|
|
166
|
+
cursor: "pointer",
|
|
167
|
+
}}
|
|
168
|
+
onClick={defaultEl.props.onClick}
|
|
169
|
+
onMouseDown={defaultEl.props.onMouseDown}
|
|
170
|
+
onMouseEnter={defaultEl.props.onMouseEnter}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
</TeethSelector>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## API Reference
|
|
181
|
+
|
|
182
|
+
### TeethSelector
|
|
183
|
+
|
|
184
|
+
The root component that manages selection state.
|
|
185
|
+
|
|
186
|
+
| Prop | Type | Description |
|
|
187
|
+
| ---------------------- | ------------------------------------------------ | ----------------------------- |
|
|
188
|
+
| `defaultSelectedTeeth` | `number[]` | Initial selected teeth IDs |
|
|
189
|
+
| `defaultBridges` | `number[][]` | Initial bridges as pairs |
|
|
190
|
+
| `selectedTeeth` | `number[]` | Controlled selected teeth |
|
|
191
|
+
| `bridges` | `number[][]` | Controlled bridges |
|
|
192
|
+
| `onSelectionChange` | `(teeth: Tooth[]) => void` | Called when selection changes |
|
|
193
|
+
| `onBridgeChange` | `(bridges: number[][]) => void` | Called when bridges change |
|
|
194
|
+
| `onChange` | `(teeth: Tooth[], bridges: number[][]) => void` | Unified change callback |
|
|
195
|
+
| `children` | `(props: TeethSelectorRenderProps) => ReactNode` | Render function |
|
|
196
|
+
|
|
197
|
+
#### Render Props
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
interface TeethSelectorRenderProps {
|
|
201
|
+
selectedTeeth: number[];
|
|
202
|
+
bridges: number[][];
|
|
203
|
+
toggleTooth: (toothId: number) => void;
|
|
204
|
+
toggleBridge: (fromId: number, toId: number) => void;
|
|
205
|
+
selectUpper: () => void;
|
|
206
|
+
selectLower: () => void;
|
|
207
|
+
selectRange: (teethIds: number[]) => void;
|
|
208
|
+
clearAll: () => void;
|
|
209
|
+
isSelected: (toothId: number) => boolean;
|
|
210
|
+
hasBridge: (fromId: number, toId: number) => boolean;
|
|
211
|
+
getSelectedTeethData: () => Tooth[];
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### TeethChart
|
|
216
|
+
|
|
217
|
+
SVG component for rendering the interactive tooth chart. Must be used inside `TeethSelector`.
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
| --------------------- | ---------- | ------- | ---------------------- |
|
|
221
|
+
| `width` | `number` | `300` | SVG width |
|
|
222
|
+
| `height` | `number` | `500` | SVG height |
|
|
223
|
+
| `className` | `string` | - | Additional class names |
|
|
224
|
+
| `enableDragSelection` | `boolean` | `true` | Enable drag to select |
|
|
225
|
+
| `renderTooth` | `function` | - | Custom tooth renderer |
|
|
226
|
+
| `renderBridge` | `function` | - | Custom bridge renderer |
|
|
227
|
+
|
|
228
|
+
### TeethPreview
|
|
229
|
+
|
|
230
|
+
Read-only component for displaying a teeth selection string.
|
|
231
|
+
|
|
232
|
+
| Prop | Type | Default | Description |
|
|
233
|
+
| -------------- | ---------- | ------- | --------------------------------------- |
|
|
234
|
+
| `teeth` | `string` | - | Selection string (e.g., "11, [21, 23]") |
|
|
235
|
+
| `width` | `number` | `150` | SVG width |
|
|
236
|
+
| `height` | `number` | `250` | SVG height |
|
|
237
|
+
| `className` | `string` | - | Additional class names |
|
|
238
|
+
| `renderTooth` | `function` | - | Custom tooth renderer |
|
|
239
|
+
| `renderBridge` | `function` | - | Custom bridge renderer |
|
|
240
|
+
|
|
241
|
+
### Utilities
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import {
|
|
245
|
+
teethData, // All 32 teeth with metadata
|
|
246
|
+
adjacentTeethPairs, // Valid bridge pairs
|
|
247
|
+
getTeethInRange, // Get teeth between two IDs
|
|
248
|
+
parseTeethSelection, // Parse string to selection
|
|
249
|
+
formatTeethSelection, // Format selection to string
|
|
250
|
+
} from "@salloomd/teeth-selector";
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Tooth Data Structure
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
interface Tooth {
|
|
257
|
+
order: number; // Display order (0-31)
|
|
258
|
+
id: number; // FDI tooth number (11-48)
|
|
259
|
+
name: string; // Full name
|
|
260
|
+
position: "upper" | "lower";
|
|
261
|
+
side: "left" | "right";
|
|
262
|
+
d: string; // SVG path data
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Dental Numbering (FDI)
|
|
267
|
+
|
|
268
|
+
The component uses FDI World Dental Federation notation:
|
|
269
|
+
|
|
270
|
+
- **Upper Right**: 18-11 (third molar to central incisor)
|
|
271
|
+
- **Upper Left**: 21-28 (central incisor to third molar)
|
|
272
|
+
- **Lower Left**: 38-31 (third molar to central incisor)
|
|
273
|
+
- **Lower Right**: 41-48 (central incisor to third molar)
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|