@artidev/odontogram-core
v0.2.1
Published
Low-level, framework-agnostic odontogram runtime for custom integrations. The runtime manages an interactive dental chart: per-tooth state, occlusal overlays, a built-in control panel, and SVG asset resolution.
Maintainers
Readme
@artidev/odontogram-core
Low-level, framework-agnostic odontogram runtime for custom integrations. The runtime manages an interactive dental chart: per-tooth state, occlusal overlays, a built-in control panel, and SVG asset resolution.
If you use Vue 3 and want a working chart in minutes, use @artidev/vue-odontogram instead. Use this package when you need direct runtime control, custom asset overrides, or a non-Vue wrapper.
Quick path
- Install the package.
- Provide a root element with the runtime hooks your wrapper needs.
- Create an instance, call
init(), and listen toonChange.
npm install @artidev/odontogram-coreimport { createOdontogramInstance } from '@artidev/odontogram-core'
const root = document.getElementById('odontogram-root')
if (!root) throw new Error('Missing #odontogram-root')
const instance = createOdontogramInstance(root, {
onChange: (state) => console.log('chart changed', state),
})
await instance.init()When to use this package
| Use it when | Don't use it when |
|---|---|
| You are building a custom wrapper around the runtime. | You want a drop-in component — use @artidev/vue-odontogram. |
| You need to override the bundled SVG assets with your own. | You're integrating from Vue without unusual needs. |
| You are integrating from a non-Vue framework (React, Svelte, vanilla). | You only need the public API — use the Vue wrapper. |
| You need raw access to init() / importState() / exportState(). | |
Distribution model
This package ships ES module source code (src/) plus the bundled SVG assets (assets/). It does not ship a pre-built bundle.
Why no pre-built bundle
The runtime resolves bundled SVG files with new URL('../assets/...', import.meta.url). Bundlers like Vite, Rollup, and Webpack transform that pattern only when they process the source file — i.e. inside your app's source graph, not inside a pre-built dist/.
The previous 0.1.x line shipped a pre-built dist/index.js. Consumer bundlers skipped the URL transform and emitted no SVG files, causing silent 404 errors in production builds. 0.2.0 fixes this by removing dist/ and shipping the source directly.
Practical implications
| Scenario | Works? |
|---|---|
| import { ... } from '@artidev/odontogram-core' in a Vite / Rollup / Webpack / Parcel app | ✅ |
| <script src="/node_modules/@artidev/odontogram-core/dist/index.js"> direct usage | ❌ No dist/ is published |
| CDN with a bundler-built artifact of your app | ✅ (use your consumer-built output) |
| Plain <script> in a static HTML page | ❌ Not supported |
package.json shape
The package now exposes:
main/module/types→./src/...exports["./assets/*"]→./assets/*(consumers can resolve individual SVG files)sideEffects: false(better tree-shaking)files→["src", "assets", "!**/*.test.ts"](no tests, no scripts ship)
Verifying the build
After npm run build in your app, check the emitted URLs:
import { defaultAssets } from '@artidev/odontogram-core'
console.log(defaultAssets.teeth[11])
// Vite dev: /node_modules/@artidev/odontogram-core/assets/teeth-svgs/11.svg
// Vite build: /assets/teeth-svgs/11-[hash].svgIf the URL is still a relative ../assets/teeth-svgs/11.svg after build, your bundler is not processing the package source — see Troubleshooting.
A note on new URL() placement
new URL(..., import.meta.url) calls in defaultAssets are intentionally inlined at the call site rather than wrapped in a helper like assetUrl(path). Vite and Rollup only transform the pattern when both arguments are statically analyzable string literals at the call site. Wrapping the path in a helper hides it from the static analyzer and silently breaks asset emission. Do not refactor defaultAssets into a helper.
API
Exports
| Export | Kind | Description |
|---|---|---|
| createOdontogramInstance | function | Creates an isolated runtime instance bound to a root element |
| defaultAssets | object | Bundled SVG asset URLs (teeth templates, occlusal overlays, icons) |
TypeScript types
import type {
OdontogramState,
SerializedToothState,
OdontogramAssets,
CreateOdontogramOptions,
OdontogramInstance,
} from '@artidev/odontogram-core'
// Record<number, SerializedToothState> — keyed by FDI tooth number
type OdontogramState = Record<number, SerializedToothState>
// Free-form per-tooth data; the runtime normalizes invalid values
type SerializedToothState = Record<string, unknown>
// Asset overrides; each slot merges on top of defaultAssets
interface OdontogramAssets {
teeth?: Partial<Record<ToothTemplateKey, string>> // 11 | 13 | 14 | 16
teethOcclusal?: Partial<Record<OcclusalTemplateKey, string>> // 14 | 16
icons?: Partial<{
occlusal: string
wisdom: string
bone: string
pulp: string
clearSelection: string
}>
}
// Instance options
interface CreateOdontogramOptions {
onChange?: (state: OdontogramState) => void
readOnly?: boolean
assets?: OdontogramAssets
}createOdontogramInstance(rootEl, options?)
Creates one isolated runtime instance bound to rootEl.
| Argument | Type | Description |
|---|---|---|
| rootEl | HTMLElement | The container the runtime mounts into |
| options | CreateOdontogramOptions \| (state) => void | Configuration object, or just an onChange callback shorthand |
Returns an OdontogramInstance.
Instance methods
| Method | Returns | Description |
|---|---|---|
| init() | Promise<void> | Builds the chart UI inside rootEl. Await before calling importState() |
| destroy() | void | Removes listeners, clears grid state, detaches the DOM. Safe to call once |
| importState(data) | void | Replaces the full chart state. Missing teeth fall back to defaults. Invalid enum values are normalized, not rejected |
| exportState() | OdontogramState | Returns the current full serialized chart state |
| setReadOnly(value) | void | Toggles readonly mode after mount. true blocks editing interactions |
State schema
OdontogramState is Record<number, SerializedToothState> keyed by FDI tooth number — the World Health Organization notation for permanent dentition: 32 teeth numbered 11..18, 21..28, 31..38, 41..48. Quadrant 1 is upper right, quadrant 4 is lower left.
Per-tooth shape
Each SerializedToothState is a free-form record the runtime normalizes on import. The runtime recognizes the following keys:
| Key | Type | Accepted values | Notes |
|---|---|---|---|
| toothSelection | string | none, tooth-base, milktooth, implant, tooth-crownprep, tooth-under-gum, no-tooth-after-extraction | milktooth is rejected on molars (16..18, 26..28, 36..38, 46..48) |
| crownMaterial | string | natural, broken, radix, emax, zircon, metal, temporary, telescope, healing-abutment, locator, locator-prosthesis, bar, bar-prosthesis | Constrained per toothSelection (implants use a subset) |
| caries | string[] | caries-subcrown, caries-buccal, caries-lingual, caries-mesial, caries-distal, caries-occlusal | Affected surfaces |
| fillingMaterial | string | none, amalgam, composite, gic, temporary | |
| fillingSurfaces | string[] | buccal, lingual, mesial, distal, occlusal | |
| fissureSealing | boolean | | Only valid on permanent molars (16, 17, 26, 27, 36, 37, 46, 47) |
| endo | string | none, endo-medical-filling, endo-filling, endo-filling-incomplete, endo-glass-pin, endo-metal-pin | |
| pulpInflam | boolean | | |
| endoResection | boolean | | |
| parapulpalPin | boolean | | |
| mobility | string | none, m1, m2, m3 | |
| mods | string[] | inflammation, parodontal, mobility | |
| contactMesial | boolean | | |
| contactDistal | boolean | | |
| bruxismWear | boolean | | Incisal wear |
| bruxismNeckWear | boolean | | Cervical wear |
| brokenMesial | boolean | | Broken-crown facet flags |
| brokenIncisal | boolean | | |
| brokenDistal | boolean | | |
| extractionWound | boolean | | |
| extractionPlan | boolean | | |
| missingClosed | boolean | | Space closed after extraction |
| bridgePillar | boolean | | |
| bridgeUnit | string | none, removable, zircon, metal, temporary, bar, bar-prosthesis | Pontic material |
| crownReplace | boolean | | |
| crownNeeded | boolean | | |
| customStates | object | | Free-form escape hatch for app-specific data |
| note | string | | |
Import semantics
importState(data)does a full replacement, not a patch.- Missing teeth in
datafall back todefaultState()(tooth-base + natural crown). - Invalid enum values are silently normalized to the closest valid value. No errors are thrown.
- The runtime coerces boolean-like strings (
"true","yes","si","sí","on") for legacy data.
Asset catalog
defaultAssets exposes:
| Key | Available values | Description |
|---|---|---|
| defaultAssets.teeth | 11, 13, 14, 16 | Tooth template SVG URLs (4 templates; the engine mirrors and rotates them to render the other 28 teeth) |
| defaultAssets.teethOcclusal | 14, 16 | Occlusal surface overlay URLs for the two occlusal templates |
| defaultAssets.icons.occlusal | string | Occlusal view toggle icon |
| defaultAssets.icons.wisdom | string | Wisdom teeth visibility toggle icon |
| defaultAssets.icons.bone | string | Bone/gum visibility toggle icon |
| defaultAssets.icons.pulp | string | Pulp visibility toggle icon |
| defaultAssets.icons.clearSelection | string | Clear selection icon |
Overriding assets
Pass a partial OdontogramAssets to createOdontogramInstance. Your override merges on top of defaultAssets, slot by slot:
import { createOdontogramInstance } from '@artidev/odontogram-core'
const instance = createOdontogramInstance(root, {
assets: {
teeth: {
16: '/my-assets/custom-molar.svg',
},
icons: {
wisdom: '/brand/icons/wisdom.svg',
bone: '/brand/icons/bone.svg',
},
},
})For full replacement of a slot, supply every key the runtime expects (see the table above).
You can also resolve individual SVG files directly via the package's exports map:
import tooth16 from '@artidev/odontogram-core/assets/teeth-svgs/16.svg'This is the recommended way to ship asset overrides through a CDN or a static asset folder.
Examples
Create an instance
import { createOdontogramInstance } from '@artidev/odontogram-core'
const root = document.getElementById('odontogram-root')
if (!root) throw new Error('Missing #odontogram-root')
const instance = createOdontogramInstance(root, {
readOnly: false,
onChange(state) {
console.log('chart changed', state)
},
})
await instance.init()Persist and restore
// Save
localStorage.setItem('odontogram', JSON.stringify(instance.exportState()))
// Restore
const saved = JSON.parse(localStorage.getItem('odontogram') ?? '{}')
instance.importState(saved)Read-only review mode
const review = createOdontogramInstance(root, {
readOnly: true,
onChange(state) {
// read-only — onChange still fires when state is imported programmatically
auditTrail.log(state)
},
})
await review.init()
review.importState({
11: { toothSelection: 'implant', crownMaterial: 'zircon' },
21: { toothSelection: 'tooth-base', crownMaterial: 'broken' },
})Asset override
const themed = createOdontogramInstance(root, {
assets: {
icons: {
wisdom: '/brand/icons/wisdom.svg',
},
},
})Load a saved chart asynchronously
async function loadChart(instance: OdontogramInstance, id: string) {
const res = await fetch(`/api/charts/${id}`)
const state: OdontogramState = await res.json()
instance.importState(state)
}Troubleshooting
SVGs return 404 in production
defaultAssets.teeth[11] resolves to a relative ../assets/... URL after build. Your bundler is not processing the package source.
Fix:
- Confirm you are not pinning
dist/index.jsanywhere (the file no longer ships). - If you use Vite, make sure
@artidev/odontogram-coreis not inoptimizeDeps.exclude. - Check that the asset URLs appear in your build manifest with hashes (
assets/teeth-svgs/11-[hash].svg). - If you are using a custom build pipeline, make sure the package is part of the source graph and not treated as pre-built.
Tree-shaking strips runtime functions
Make sure your bundler respects sideEffects: false. Webpack 5 does this by default when reading package.json#sideEffects.
TypeScript types missing
If tsc cannot find the types, ensure moduleResolution is node16, nodenext, or bundler. The package exposes .d.ts next to the .js source.
Refactor broke SVG emission
If you wrap the new URL() calls in default-assets.js in a helper function, asset emission will break silently. The path argument must be a string literal at the call site. See Distribution model.
Migration from 0.1.x
| Before (0.1.x) | After (0.2.0) |
|---|---|
| import { ... } from '@artidev/odontogram-core/dist/index.js' | import { ... } from '@artidev/odontogram-core' |
| <script src=".../dist/index.js"> (UMD-ish direct usage) | Not supported. Build with a bundler. |
| dist/ present in the published tarball | Gone — only src/ and assets/ ship. |
There are no API changes. If your bundler processed node_modules correctly in 0.1.x, you may have had broken SVG URLs without knowing it — the fix is automatic with 0.2.0.
Related packages
@artidev/vue-odontogram— ready-to-use Vue 3 wrapper around this runtime.
License
MIT
