odontogram 0.0.1 → 0.1.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.
Files changed (54) hide show
  1. package/dist/index.d.mts +35 -0
  2. package/dist/index.mjs +297 -0
  3. package/package.json +45 -99
  4. package/src/index.ts +2 -0
  5. package/src/og-odontogram.ts +109 -0
  6. package/src/og-tooth.ts +76 -0
  7. package/src/stories/Layout.stories.ts +64 -0
  8. package/src/stories/Odontogram.stories.ts +68 -0
  9. package/LICENSE +0 -21
  10. package/README.md +0 -299
  11. package/cdn/components/button/button.d.ts +0 -25
  12. package/cdn/components/button/button.d.ts.map +0 -1
  13. package/cdn/components/button/button.js +0 -598
  14. package/cdn/components/button/button.styles.d.ts +0 -3
  15. package/cdn/components/button/button.styles.d.ts.map +0 -1
  16. package/cdn/components/button/index.d.ts +0 -2
  17. package/cdn/components/button/index.d.ts.map +0 -1
  18. package/cdn/components/button/index.js +0 -2
  19. package/cdn/components/index.d.ts +0 -2
  20. package/cdn/components/index.d.ts.map +0 -1
  21. package/cdn/components/index.js +0 -1
  22. package/cdn/index.d.ts +0 -2
  23. package/cdn/index.d.ts.map +0 -1
  24. package/cdn/index.js +0 -1
  25. package/cdn/loader.js +0 -118
  26. package/custom-elements.json +0 -152
  27. package/dist/components/button/button.d.ts +0 -25
  28. package/dist/components/button/button.d.ts.map +0 -1
  29. package/dist/components/button/button.js +0 -47
  30. package/dist/components/button/button.js.map +0 -1
  31. package/dist/components/button/button.styles.d.ts +0 -3
  32. package/dist/components/button/button.styles.d.ts.map +0 -1
  33. package/dist/components/button/button.styles.js +0 -43
  34. package/dist/components/button/button.styles.js.map +0 -1
  35. package/dist/components/button/index.d.ts +0 -2
  36. package/dist/components/button/index.d.ts.map +0 -1
  37. package/dist/components/button/index.js +0 -3
  38. package/dist/components/button/index.js.map +0 -1
  39. package/dist/components/index.d.ts +0 -2
  40. package/dist/components/index.d.ts.map +0 -1
  41. package/dist/components/index.js +0 -2
  42. package/dist/components/index.js.map +0 -1
  43. package/dist/index.d.ts +0 -2
  44. package/dist/index.d.ts.map +0 -1
  45. package/dist/index.js +0 -2
  46. package/dist/index.js.map +0 -1
  47. package/react/MyButton.d.ts +0 -90
  48. package/react/MyButton.js +0 -32
  49. package/react/index.d.ts +0 -1
  50. package/react/index.js +0 -1
  51. package/react/react-utils.js +0 -67
  52. package/types/custom-element-jsx.d.ts +0 -236
  53. package/types/custom-element-svelte.d.ts +0 -70
  54. package/types/custom-element-vuejs.d.ts +0 -40
@@ -0,0 +1,35 @@
1
+ import * as lit from "lit";
2
+ import { LitElement } from "lit";
3
+
4
+ //#region src/og-tooth.d.ts
5
+ interface ToothSurfaces {
6
+ vestibular: boolean;
7
+ distal: boolean;
8
+ palatine: boolean;
9
+ mesial: boolean;
10
+ occlusal: boolean;
11
+ }
12
+ declare class OgTooth extends LitElement {
13
+ toothId: string;
14
+ colorClick: string;
15
+ selections: ToothSurfaces;
16
+ private _toggle;
17
+ render(): lit.TemplateResult<1>;
18
+ static styles: lit.CSSResult;
19
+ }
20
+ //#endregion
21
+ //#region src/og-odontogram.d.ts
22
+ type PatientMode = 'adult' | 'baby' | 'old';
23
+ declare class OgDontogram extends LitElement {
24
+ mode: PatientMode;
25
+ chartData: Record<string, ToothSurfaces>;
26
+ private teethState;
27
+ private layouts;
28
+ willUpdate(changedProperties: Map<string, any>): void;
29
+ private _updateState;
30
+ renderTooth(id: number): lit.TemplateResult<1>;
31
+ render(): lit.TemplateResult<1>;
32
+ static styles: lit.CSSResult;
33
+ }
34
+ //#endregion
35
+ export { OgDontogram, OgTooth, PatientMode, ToothSurfaces };
package/dist/index.mjs ADDED
@@ -0,0 +1,297 @@
1
+ import { LitElement, css, html } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+
4
+ //#region \0@oxc-project+runtime@0.114.0/helpers/decorate.js
5
+ function __decorate(decorators, target, key, desc) {
6
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
8
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
10
+ }
11
+
12
+ //#endregion
13
+ //#region src/og-tooth.ts
14
+ let OgTooth = class OgTooth extends LitElement {
15
+ constructor(..._args) {
16
+ super(..._args);
17
+ this.toothId = "";
18
+ this.colorClick = "#ff6961";
19
+ this.selections = {
20
+ vestibular: false,
21
+ distal: false,
22
+ palatine: false,
23
+ mesial: false,
24
+ occlusal: false
25
+ };
26
+ }
27
+ _toggle(surface, clickFn, unclickFn) {
28
+ const isSelected = !this.selections[surface];
29
+ this.selections = {
30
+ ...this.selections,
31
+ [surface]: isSelected
32
+ };
33
+ const eventName = isSelected ? clickFn : unclickFn;
34
+ this.dispatchEvent(new CustomEvent(eventName, {
35
+ detail: {
36
+ toothId: this.toothId,
37
+ surface,
38
+ state: isSelected
39
+ },
40
+ bubbles: true,
41
+ composed: true
42
+ }));
43
+ }
44
+ render() {
45
+ return html`
46
+ <div class="tooth-container">
47
+ <span class="label">${this.toothId}</span>
48
+ <div class="tooth-relative">
49
+ <div class="surface top ${this.selections.vestibular ? "selected" : ""}"
50
+ @click=${() => this._toggle("vestibular", "vestibularC", "vestibularU")}></div>
51
+
52
+ <div class="surface left ${this.selections.distal ? "selected" : ""}"
53
+ @click=${() => this._toggle("distal", "distalC", "distalU")}></div>
54
+
55
+ <div class="surface bottom ${this.selections.palatine ? "selected" : ""}"
56
+ @click=${() => this._toggle("palatine", "palatineC", "palatineU")}></div>
57
+
58
+ <div class="surface right ${this.selections.mesial ? "selected" : ""}"
59
+ @click=${() => this._toggle("mesial", "mesialC", "mesialU")}></div>
60
+
61
+ <div class="surface center ${this.selections.occlusal ? "selected" : ""}"
62
+ @click=${() => this._toggle("occlusal", "occlusalC", "occlusalU")}></div>
63
+ </div>
64
+ </div>
65
+ `;
66
+ }
67
+ static {
68
+ this.styles = css`
69
+ .tooth-container { display: flex; flex-direction: column; align-items: center; width: 50px; }
70
+ .label { font-size: 12px; margin-bottom: 5px; font-weight: bold; color: #333; }
71
+ .tooth-relative { position: relative; width: 44px; height: 44px; background: #eee; }
72
+ .surface {
73
+ position: absolute; width: 20px; height: 20px; outline: 2px solid #000;
74
+ background-color: #fff; cursor: pointer; box-sizing: border-box; transition: background-color 0.2s;
75
+ }
76
+ .selected { background-color: var(--click-color, #ff6961) !important; }
77
+ .top { top: 0; left: 0; border-top-left-radius: 100%; }
78
+ .left { bottom: 0; left: 0; border-bottom-left-radius: 100%; }
79
+ .bottom { bottom: 0; right: 0; border-bottom-right-radius: 100%; }
80
+ .right { top: 0; right: 0; border-top-right-radius: 100%; }
81
+ .center { top: 25%; right: 25%; border-radius: 50%; z-index: 2; }
82
+ `;
83
+ }
84
+ };
85
+ __decorate([property({ type: String })], OgTooth.prototype, "toothId", void 0);
86
+ __decorate([property({ type: String })], OgTooth.prototype, "colorClick", void 0);
87
+ __decorate([property({ type: Object })], OgTooth.prototype, "selections", void 0);
88
+ OgTooth = __decorate([customElement("og-tooth")], OgTooth);
89
+
90
+ //#endregion
91
+ //#region src/og-odontogram.ts
92
+ let OgDontogram = class OgDontogram extends LitElement {
93
+ constructor(..._args) {
94
+ super(..._args);
95
+ this.mode = "adult";
96
+ this.chartData = {};
97
+ this.teethState = {};
98
+ this.layouts = {
99
+ adult: {
100
+ upperRight: [
101
+ 18,
102
+ 17,
103
+ 16,
104
+ 15,
105
+ 14,
106
+ 13,
107
+ 12,
108
+ 11
109
+ ],
110
+ upperLeft: [
111
+ 21,
112
+ 22,
113
+ 23,
114
+ 24,
115
+ 25,
116
+ 26,
117
+ 27,
118
+ 28
119
+ ],
120
+ lowerRight: [
121
+ 48,
122
+ 47,
123
+ 46,
124
+ 45,
125
+ 44,
126
+ 43,
127
+ 42,
128
+ 41
129
+ ],
130
+ lowerLeft: [
131
+ 31,
132
+ 32,
133
+ 33,
134
+ 34,
135
+ 35,
136
+ 36,
137
+ 37,
138
+ 38
139
+ ]
140
+ },
141
+ baby: {
142
+ upperRight: [
143
+ 55,
144
+ 54,
145
+ 53,
146
+ 52,
147
+ 51
148
+ ],
149
+ upperLeft: [
150
+ 61,
151
+ 62,
152
+ 63,
153
+ 64,
154
+ 65
155
+ ],
156
+ lowerRight: [
157
+ 85,
158
+ 84,
159
+ 83,
160
+ 82,
161
+ 81
162
+ ],
163
+ lowerLeft: [
164
+ 71,
165
+ 72,
166
+ 73,
167
+ 74,
168
+ 75
169
+ ]
170
+ },
171
+ old: {
172
+ upperRight: [
173
+ 17,
174
+ 16,
175
+ 15,
176
+ 14,
177
+ 13,
178
+ 12,
179
+ 11
180
+ ],
181
+ upperLeft: [
182
+ 21,
183
+ 22,
184
+ 23,
185
+ 24,
186
+ 25,
187
+ 26,
188
+ 27
189
+ ],
190
+ lowerRight: [
191
+ 47,
192
+ 46,
193
+ 45,
194
+ 44,
195
+ 43,
196
+ 42,
197
+ 41
198
+ ],
199
+ lowerLeft: [
200
+ 31,
201
+ 32,
202
+ 33,
203
+ 34,
204
+ 35,
205
+ 36,
206
+ 37
207
+ ]
208
+ }
209
+ };
210
+ }
211
+ willUpdate(changedProperties) {
212
+ if (changedProperties.has("chartData")) this.teethState = { ...this.chartData };
213
+ }
214
+ _updateState(id, region, value) {
215
+ const toothId = id.toString();
216
+ const newState = { ...this.teethState };
217
+ if (!newState[toothId]) newState[toothId] = {
218
+ vestibular: false,
219
+ distal: false,
220
+ palatine: false,
221
+ mesial: false,
222
+ occlusal: false
223
+ };
224
+ newState[toothId] = {
225
+ ...newState[toothId],
226
+ [region]: value
227
+ };
228
+ this.teethState = newState;
229
+ this.dispatchEvent(new CustomEvent("odontogram-change", {
230
+ detail: {
231
+ data: this.teethState,
232
+ mode: this.mode
233
+ },
234
+ bubbles: true,
235
+ composed: true
236
+ }));
237
+ }
238
+ renderTooth(id) {
239
+ const state = this.teethState[id.toString()] || {
240
+ vestibular: false,
241
+ distal: false,
242
+ palatine: false,
243
+ mesial: false,
244
+ occlusal: false
245
+ };
246
+ return html`
247
+ <og-tooth
248
+ .toothId=${id.toString()}
249
+ .selections=${state}
250
+ @vestibularC=${() => this._updateState(id, "vestibular", true)}
251
+ @vestibularU=${() => this._updateState(id, "vestibular", false)}
252
+ @distalC=${() => this._updateState(id, "distal", true)}
253
+ @distalU=${() => this._updateState(id, "distal", false)}
254
+ @palatineC=${() => this._updateState(id, "palatine", true)}
255
+ @palatineU=${() => this._updateState(id, "palatine", false)}
256
+ @mesialC=${() => this._updateState(id, "mesial", true)}
257
+ @mesialU=${() => this._updateState(id, "mesial", false)}
258
+ @occlusalC=${() => this._updateState(id, "occlusal", true)}
259
+ @occlusalU=${() => this._updateState(id, "occlusal", false)}
260
+ ></og-tooth>
261
+ `;
262
+ }
263
+ render() {
264
+ const layout = this.layouts[this.mode] || this.layouts.adult;
265
+ return html`
266
+ <div class="odontogram-wrapper mode-${this.mode}">
267
+ <div class="arch">
268
+ <div class="quadrant">${layout.upperRight.map((id) => this.renderTooth(id))}</div>
269
+ <div class="quadrant">${layout.upperLeft.map((id) => this.renderTooth(id))}</div>
270
+ </div>
271
+ <div class="arch">
272
+ <div class="quadrant">${layout.lowerRight.map((id) => this.renderTooth(id))}</div>
273
+ <div class="quadrant">${layout.lowerLeft.map((id) => this.renderTooth(id))}</div>
274
+ </div>
275
+ </div>
276
+ `;
277
+ }
278
+ static {
279
+ this.styles = css`
280
+ :host { display: block; padding: 20px; }
281
+ .odontogram-wrapper { display: flex; flex-direction: column; gap: 40px; align-items: center; }
282
+ .arch { display: flex; gap: 40px; }
283
+ .quadrant { display: flex; gap: 4px; }
284
+
285
+ /* You can add specific colors or spacing per mode if you want */
286
+ .mode-baby { gap: 20px; }
287
+ .mode-baby og-tooth { transform: scale(0.9); }
288
+ `;
289
+ }
290
+ };
291
+ __decorate([property({ type: String })], OgDontogram.prototype, "mode", void 0);
292
+ __decorate([property({ type: Object })], OgDontogram.prototype, "chartData", void 0);
293
+ __decorate([state()], OgDontogram.prototype, "teethState", void 0);
294
+ OgDontogram = __decorate([customElement("og-dontogram")], OgDontogram);
295
+
296
+ //#endregion
297
+ export { OgDontogram, OgTooth };
package/package.json CHANGED
@@ -1,120 +1,66 @@
1
1
  {
2
2
  "name": "odontogram",
3
- "version": "0.0.1",
4
- "description": "An Simple Web Component library for odontogram",
5
- "author": "Pratik Sharma <sharma.pratik2016@gmail.com>",
3
+ "version": "0.1.1",
4
+ "description": "A lightweight, interactive web component odontogram built with Lit.",
5
+ "main": "./dist/index.mjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "sideEffects": [
19
+ "**/*.js",
20
+ "**/*.ts"
21
+ ],
22
+ "peerDependencies": {
23
+ "lit": "^3.0.0"
24
+ },
6
25
  "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
7
29
  "keywords": [
8
30
  "odontogram",
9
31
  "react",
10
32
  "dental-chart"
11
33
  ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
34
  "repository": {
16
35
  "type": "git",
17
36
  "url": "https://github.com/biomathcode/odontogram"
18
37
  },
19
- "main": "dist/index.js",
20
38
  "type": "module",
21
39
  "scripts": {
22
- "analyze": "cem analyze",
23
- "analyze:dev": "cem analyze --watch",
24
- "clean": "git clean -fqdx",
25
- "dev": "npm run build && concurrently -k -r \"npm run analyze:dev\" \"npm run build:watch\" \"npm run storybook\"",
26
- "test": "web-test-runner --group default",
27
- "build": "tsc && npm run analyze && npm run build:kitchen-sink",
28
- "build:cdn": "npx cross-env BUILD_TARGET=cdn vite build && npm run analyze",
29
- "build:html": "npx cross-env BUILD_TARGET=html vite build",
30
- "build:react": "npx cross-env BUILD_TARGET=react vite build && npx rimraf -rf ./public/react/html",
31
- "build:kitchen-sink": "npx rimraf ./public && npm run build:cdn && npm run build:html && npm run build:react",
32
- "build:watch": "concurrently -k -r \"tsc --watch\" \"npx cross-env BUILD_TARGET=cdn vite build --watch\"",
33
- "new": "plop",
34
- "deploy": "npm run build && npm publish",
35
- "format": "npm run format:eslint && npm run format:prettier",
36
- "format:eslint": "npx eslint --fix",
37
- "format:prettier": "npx prettier . --write",
38
- "lint": "wctools validate && npm run lint:eslint && npm run lint:prettier",
39
- "lint:eslint": "npx eslint",
40
- "lint:prettier": "npx prettier . --check",
41
- "prepare": "husky && npx playwright install-deps",
40
+ "dev": "vite",
41
+ "build": "tsdown",
42
+ "build:vite": "tsc && vite build",
43
+ "preview": "vite preview",
42
44
  "storybook": "storybook dev -p 6006",
43
45
  "build-storybook": "storybook build"
44
46
  },
45
47
  "dependencies": {
46
- "code-bubble": "^1.3.3",
47
- "lit": "^3.2.1",
48
- "wc-dox": "^1.3.5"
48
+ "lit": "^3.3.1"
49
49
  },
50
50
  "devDependencies": {
51
- "@custom-elements-manifest/analyzer": "^0.11.0",
52
- "@eslint/js": "^9.16.0",
53
- "@eslint/json": "^0.8.0",
54
- "@eslint/markdown": "^6.2.1",
55
- "@open-wc/testing": "^4.0.0",
56
- "@playwright/test": "^1.46.1",
57
- "@storybook/addon-a11y": "^10.1.11",
58
- "@storybook/addon-docs": "^10.1.11",
59
- "@storybook/addon-links": "^10.1.11",
60
- "@storybook/web-components": "^10.1.11",
61
- "@storybook/web-components-vite": "^10.1.11",
62
- "@types/mocha": "^10.0.2",
63
- "@wc-toolkit/cem-inheritance": "^1.2.2",
64
- "@wc-toolkit/cem-sorter": "^1.0.1",
65
- "@wc-toolkit/cem-validator": "^1.0.3",
66
- "@wc-toolkit/jsdoc-tags": "^1.1.0",
67
- "@wc-toolkit/jsx-types": "^1.5.2",
68
- "@wc-toolkit/lazy-loader": "^1.0.1",
69
- "@wc-toolkit/module-path-resolver": "^1.0.0",
70
- "@wc-toolkit/react-wrappers": "^1.1.1",
71
- "@wc-toolkit/storybook-helpers": "^10.0.0",
72
- "@wc-toolkit/type-parser": "^1.2.0",
73
- "@wc-toolkit/wctools": "^0.0.18",
74
- "@web/dev-server-esbuild": "^1.0.4",
75
- "@web/test-runner": "^0.20.2",
76
- "@web/test-runner-commands": "^0.9.0",
77
- "@web/test-runner-playwright": "^0.11.1",
78
- "concurrently": "^9.1.0",
79
- "custom-element-svelte-integration": "^1.1.2",
80
- "custom-element-vuejs-integration": "^1.3.3",
81
- "custom-elements-manifest-deprecator": "^1.1.1",
82
- "eslint": "^9.16.0",
83
- "eslint-config-prettier": "^9.1.0",
84
- "eslint-plugin-lit": "^1.15.0",
85
- "eslint-plugin-lit-a11y": "^5.1.1",
86
- "eslint-plugin-require-extensions": "^0.1.3",
87
- "eslint-plugin-storybook": "^10.1.11",
88
- "glob": "^11.0.0",
89
- "globals": "^15.13.0",
90
- "husky": "^9.0.11",
91
- "lint-staged": "^15.2.7",
92
- "plop": "^4.0.1",
93
- "prettier": "^3.3.2",
94
- "rollup-plugin-summary": "^3.0.0",
95
- "storybook": "^10.1.11",
96
- "typescript": "^5.5.3",
97
- "typescript-eslint": "^8.17.0",
98
- "vite": "^7.3.0",
99
- "vite-plugin-dts": "^4.5.4"
100
- },
101
- "lint-staged": {
102
- "*.js": "eslint --cache --fix",
103
- "*.format:prettier": "prettier --write"
104
- },
105
- "files": [
106
- "cdn",
107
- "eslint",
108
- "dist",
109
- "react",
110
- "types",
111
- "index.d.ts",
112
- "index.js",
113
- "package.json",
114
- "custom-elements.json",
115
- "vscode.css-custom-data.json",
116
- "vscode.html-custom-data.json",
117
- "web-types.json"
118
- ],
119
- "customElements": "custom-elements.json"
51
+ "@chromatic-com/storybook": "^5.0.1",
52
+ "@storybook/addon-a11y": "^10.2.13",
53
+ "@storybook/addon-docs": "^10.2.13",
54
+ "@storybook/addon-vitest": "^10.2.13",
55
+ "@storybook/web-components-vite": "^10.2.13",
56
+ "@vitest/browser-playwright": "^4.0.18",
57
+ "@vitest/coverage-v8": "^4.0.18",
58
+ "playwright": "^1.58.2",
59
+ "storybook": "^10.2.13",
60
+ "tsdown": "0.21.0-beta.2",
61
+ "typescript": "~5.9.3",
62
+ "vite": "^7.3.1",
63
+ "vite-plugin-dts": "^4.5.4",
64
+ "vitest": "^4.0.18"
65
+ }
120
66
  }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './og-tooth';
2
+ export * from './og-odontogram';
@@ -0,0 +1,109 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import type { ToothSurfaces } from './og-tooth';
4
+ import './og-tooth';
5
+
6
+ export type PatientMode = 'adult' | 'baby' | 'old';
7
+
8
+ @customElement('og-dontogram')
9
+ export class OgDontogram extends LitElement {
10
+ @property({ type: String }) mode: PatientMode = 'adult';
11
+ @property({ type: Object }) chartData: Record<string, ToothSurfaces> = {};
12
+ @state() private teethState: Record<string, ToothSurfaces> = {};
13
+
14
+ // FDI Tooth Layouts
15
+ private layouts = {
16
+ adult: {
17
+ upperRight: [18, 17, 16, 15, 14, 13, 12, 11],
18
+ upperLeft: [21, 22, 23, 24, 25, 26, 27, 28],
19
+ lowerRight: [48, 47, 46, 45, 44, 43, 42, 41],
20
+ lowerLeft: [31, 32, 33, 34, 35, 36, 37, 38]
21
+ },
22
+ baby: {
23
+ upperRight: [55, 54, 53, 52, 51],
24
+ upperLeft: [61, 62, 63, 64, 65],
25
+ lowerRight: [85, 84, 83, 82, 81],
26
+ lowerLeft: [71, 72, 73, 74, 75]
27
+ },
28
+ old: {
29
+ // Typically adult layout, but maybe we exclude wisdom teeth (18, 28, 38, 48)
30
+ upperRight: [17, 16, 15, 14, 13, 12, 11],
31
+ upperLeft: [21, 22, 23, 24, 25, 26, 27],
32
+ lowerRight: [47, 46, 45, 44, 43, 42, 41],
33
+ lowerLeft: [31, 32, 33, 34, 35, 36, 37]
34
+ }
35
+ };
36
+
37
+ willUpdate(changedProperties: Map<string, any>) {
38
+ if (changedProperties.has('chartData')) {
39
+ this.teethState = { ...this.chartData };
40
+ }
41
+ }
42
+
43
+ private _updateState(id: number, region: keyof ToothSurfaces, value: boolean) {
44
+ const toothId = id.toString();
45
+ const newState = { ...this.teethState };
46
+ if (!newState[toothId]) {
47
+ newState[toothId] = { vestibular: false, distal: false, palatine: false, mesial: false, occlusal: false };
48
+ }
49
+ newState[toothId] = { ...newState[toothId], [region]: value };
50
+ this.teethState = newState;
51
+
52
+ this.dispatchEvent(new CustomEvent('odontogram-change', {
53
+ detail: { data: this.teethState, mode: this.mode },
54
+ bubbles: true,
55
+ composed: true
56
+ }));
57
+ }
58
+
59
+ renderTooth(id: number) {
60
+ const state = this.teethState[id.toString()] || {
61
+ vestibular: false, distal: false, palatine: false, mesial: false, occlusal: false
62
+ };
63
+
64
+ return html`
65
+ <og-tooth
66
+ .toothId=${id.toString()}
67
+ .selections=${state}
68
+ @vestibularC=${() => this._updateState(id, 'vestibular', true)}
69
+ @vestibularU=${() => this._updateState(id, 'vestibular', false)}
70
+ @distalC=${() => this._updateState(id, 'distal', true)}
71
+ @distalU=${() => this._updateState(id, 'distal', false)}
72
+ @palatineC=${() => this._updateState(id, 'palatine', true)}
73
+ @palatineU=${() => this._updateState(id, 'palatine', false)}
74
+ @mesialC=${() => this._updateState(id, 'mesial', true)}
75
+ @mesialU=${() => this._updateState(id, 'mesial', false)}
76
+ @occlusalC=${() => this._updateState(id, 'occlusal', true)}
77
+ @occlusalU=${() => this._updateState(id, 'occlusal', false)}
78
+ ></og-tooth>
79
+ `;
80
+ }
81
+
82
+ render() {
83
+ const layout = this.layouts[this.mode] || this.layouts.adult;
84
+
85
+ return html`
86
+ <div class="odontogram-wrapper mode-${this.mode}">
87
+ <div class="arch">
88
+ <div class="quadrant">${layout.upperRight.map(id => this.renderTooth(id))}</div>
89
+ <div class="quadrant">${layout.upperLeft.map(id => this.renderTooth(id))}</div>
90
+ </div>
91
+ <div class="arch">
92
+ <div class="quadrant">${layout.lowerRight.map(id => this.renderTooth(id))}</div>
93
+ <div class="quadrant">${layout.lowerLeft.map(id => this.renderTooth(id))}</div>
94
+ </div>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ static styles = css`
100
+ :host { display: block; padding: 20px; }
101
+ .odontogram-wrapper { display: flex; flex-direction: column; gap: 40px; align-items: center; }
102
+ .arch { display: flex; gap: 40px; }
103
+ .quadrant { display: flex; gap: 4px; }
104
+
105
+ /* You can add specific colors or spacing per mode if you want */
106
+ .mode-baby { gap: 20px; }
107
+ .mode-baby og-tooth { transform: scale(0.9); }
108
+ `;
109
+ }
@@ -0,0 +1,76 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ // Shared Interface for Type Safety
5
+ export interface ToothSurfaces {
6
+ vestibular: boolean;
7
+ distal: boolean;
8
+ palatine: boolean;
9
+ mesial: boolean;
10
+ occlusal: boolean;
11
+ }
12
+
13
+ @customElement('og-tooth')
14
+ export class OgTooth extends LitElement {
15
+ @property({ type: String }) toothId = '';
16
+ @property({ type: String }) colorClick = '#ff6961';
17
+
18
+ @property({ type: Object })
19
+ selections: ToothSurfaces = {
20
+ vestibular: false, distal: false, palatine: false, mesial: false, occlusal: false
21
+ };
22
+
23
+ private _toggle(surface: keyof ToothSurfaces, clickFn: string, unclickFn: string) {
24
+ const isSelected = !this.selections[surface];
25
+
26
+ // Create new object for Lit reactivity
27
+ this.selections = { ...this.selections, [surface]: isSelected };
28
+
29
+ const eventName = isSelected ? clickFn : unclickFn;
30
+ this.dispatchEvent(new CustomEvent(eventName, {
31
+ detail: { toothId: this.toothId, surface, state: isSelected },
32
+ bubbles: true,
33
+ composed: true
34
+ }));
35
+ }
36
+
37
+ render() {
38
+ return html`
39
+ <div class="tooth-container">
40
+ <span class="label">${this.toothId}</span>
41
+ <div class="tooth-relative">
42
+ <div class="surface top ${this.selections.vestibular ? 'selected' : ''}"
43
+ @click=${() => this._toggle('vestibular', 'vestibularC', 'vestibularU')}></div>
44
+
45
+ <div class="surface left ${this.selections.distal ? 'selected' : ''}"
46
+ @click=${() => this._toggle('distal', 'distalC', 'distalU')}></div>
47
+
48
+ <div class="surface bottom ${this.selections.palatine ? 'selected' : ''}"
49
+ @click=${() => this._toggle('palatine', 'palatineC', 'palatineU')}></div>
50
+
51
+ <div class="surface right ${this.selections.mesial ? 'selected' : ''}"
52
+ @click=${() => this._toggle('mesial', 'mesialC', 'mesialU')}></div>
53
+
54
+ <div class="surface center ${this.selections.occlusal ? 'selected' : ''}"
55
+ @click=${() => this._toggle('occlusal', 'occlusalC', 'occlusalU')}></div>
56
+ </div>
57
+ </div>
58
+ `;
59
+ }
60
+
61
+ static styles = css`
62
+ .tooth-container { display: flex; flex-direction: column; align-items: center; width: 50px; }
63
+ .label { font-size: 12px; margin-bottom: 5px; font-weight: bold; color: #333; }
64
+ .tooth-relative { position: relative; width: 44px; height: 44px; background: #eee; }
65
+ .surface {
66
+ position: absolute; width: 20px; height: 20px; outline: 2px solid #000;
67
+ background-color: #fff; cursor: pointer; box-sizing: border-box; transition: background-color 0.2s;
68
+ }
69
+ .selected { background-color: var(--click-color, #ff6961) !important; }
70
+ .top { top: 0; left: 0; border-top-left-radius: 100%; }
71
+ .left { bottom: 0; left: 0; border-bottom-left-radius: 100%; }
72
+ .bottom { bottom: 0; right: 0; border-bottom-right-radius: 100%; }
73
+ .right { top: 0; right: 0; border-top-right-radius: 100%; }
74
+ .center { top: 25%; right: 25%; border-radius: 50%; z-index: 2; }
75
+ `;
76
+ }