diff --git a/README.md b/README.md index 0ac8b0a..3a9e42d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ yarn add react-odontogram ```tsx import { Odontogram } from "react-odontogram"; +import "react-odontogram/style.css"; export default function App() { const handleChange = (selectedTeeth) => { @@ -129,12 +130,19 @@ Example JSON output: ## ⚙️ Props -| Prop | Type | Default | Description | -| ----------------- | ------------------------------------------- | ------- | ------------------------------------------------------- | -| `onChange` | `(selectedTeeth: ToothSelection[]) => void` | — | Triggered whenever the user selects or deselects teeth. | -| `initialSelected` | `string[]` | `[]` | Array of tooth IDs to preselect. | -| `readOnly` | `boolean` | `false` | Makes the odontogram non-interactive (view-only). | -| `className` | `string` | — | Optional class for custom styling. | +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `defaultSelected` | `string[]` | `[]` | Tooth IDs selected on first render. | +| `onChange` | `(selectedTeeth: ToothSelection[]) => void` | — | Called whenever selection changes. | +| `name` | `string` | `"teeth"` | Name used for hidden form input. | +| `className` | `string` | `""` | Additional class for wrapper customization. | +| `theme` | `"light" \| "dark"` | `"light"` | Applies built-in light/dark palette. | +| `colors` | `{ darkBlue?: string; baseBlue?: string; lightBlue?: string }` | `{}` | Override palette colors. | +| `notation` | `"FDI" \| "Universal" \| "Palmer"` | `"FDI"` | Display notation in native tooth titles/tooltips. | +| `tooltip` | `{ placement?: Placement; margin?: number; content?: ReactNode \| ((payload?: ToothSelection) => ReactNode) }` | `{ placement: "top", margin: 10 }` | Tooltip behavior and custom content renderer. | +| `showTooltip` | `boolean` | `true` | Enables/disables tooltip rendering. | +| `showHalf` | `"full" \| "upper" \| "lower"` | `"full"` | Render full chart or only upper/lower arches. | +| `maxTeeth` | `number` | `8` | Number of teeth per quadrant (for baby/mixed dentition views). | --- @@ -144,14 +152,8 @@ Each tooth is internally defined in a structured format: ```ts { - id: "teeth-21", - name: "21", + name: "1", type: "Central Incisor", - notations: { - fdi: "21", - universal: "9", - palmer: "1UL" - }, outlinePath: "...", shadowPath: "...", lineHighlightPath: "..." @@ -190,5 +192,3 @@ MIT © [biomathcode](https://github.com/biomathcode) ## 💬 Feedback If this library helps your dental project, please ⭐ the repo or open issues/PRs for enhancements! - - diff --git a/lefthook.yml b/lefthook.yml index 7cba3ed..d5e2f70 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,8 +4,8 @@ pre-commit: lint: run: git add -u typecheck: - run: pnpm tsc + run: npx pnpm tsc build: - run: pnpm build + run: npx pnpm build test: - run: pnpm test:ci + run: npx pnpm test:ci diff --git a/package.json b/package.json index a89ad59..87806c7 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,17 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "require": "./dist/index.js", - "import": "./dist/index.mjs" - } + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./style.css": "./dist/index.css" }, + "sideEffects": [ + "*.css", + "**/*.css" + ], "files": [ "dist" ], @@ -65,11 +72,11 @@ "@biomejs/biome": "1.9.4", "@ryansonshine/commitizen": "4.2.8", "@ryansonshine/cz-conventional-changelog": "3.3.4", - "@storybook/addon-a11y": "^10.0.8", - "@storybook/addon-links": "10.0.8", - "@storybook/addon-themes": "^10.0.8", + "@storybook/addon-a11y": "^10.2.10", + "@storybook/addon-links": "10.2.10", + "@storybook/addon-themes": "^10.2.10", "@storybook/addon-webpack5-compiler-swc": "4.0.2", - "@storybook/react-webpack5": "10.0.8", + "@storybook/react-webpack5": "10.2.10", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.2.0", "@types/node": "22.13.11", @@ -87,14 +94,14 @@ "react-dom": "18.3.1", "react-test-renderer": "18.3.1", "release-it": "18.1.2", - "storybook": "10.0.8", + "storybook": "10.2.10", "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", "tsup": "8.4.0", "tsx": "4.19.3", "typescript": "5.8.2", "vitest": "3.0.9", - "@storybook/addon-docs": "10.0.8" + "@storybook/addon-docs": "10.2.10" }, "peerDependencies": { "react": ">=17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 356253e..cccdbb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,23 +21,23 @@ importers: specifier: 3.3.4 version: 3.3.4(@types/node@22.13.11)(typescript@5.8.2) '@storybook/addon-a11y': - specifier: ^10.0.8 - version: 10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + specifier: ^10.2.10 + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-docs': - specifier: 10.0.8 - version: 10.0.8(@types/react@18.3.13)(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) + specifier: 10.2.10 + version: 10.2.10(@types/react@18.3.13)(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) '@storybook/addon-links': - specifier: 10.0.8 - version: 10.0.8(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + specifier: 10.2.10 + version: 10.2.10(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-themes': - specifier: ^10.0.8 - version: 10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + specifier: ^10.2.10 + version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-webpack5-compiler-swc': specifier: 4.0.2 - version: 4.0.2(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) + version: 4.0.2(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) '@storybook/react-webpack5': - specifier: 10.0.8 - version: 10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2) + specifier: 10.2.10 + version: 10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2) '@testing-library/jest-dom': specifier: 6.6.3 version: 6.6.3 @@ -90,8 +90,8 @@ importers: specifier: 18.1.2 version: 18.1.2(@types/node@22.13.11)(typescript@5.8.2) storybook: - specifier: 10.0.8 - version: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + specifier: 10.2.10 + version: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-node: specifier: 10.9.2 version: 10.9.2(@swc/core@1.15.3)(@types/node@22.13.11)(typescript@5.8.2) @@ -127,53 +127,108 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.8': resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.26.10': resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.26.10': resolution: {integrity: sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.26.5': resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.9': resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.26.0': resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.26.10': resolution: {integrity: sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.26.10': resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.26.10': resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} @@ -182,14 +237,26 @@ packages: resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.10': resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.26.10': resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -731,6 +798,9 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -755,6 +825,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -1054,29 +1127,29 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@storybook/addon-a11y@10.0.8': - resolution: {integrity: sha512-rXJuSfujuRqkz1v26wLttVRkXzZur3LtMTH1/K+rb1epXq305vRm/bYql0FYGvrq0idJWYo4WbU65YRig9sfuA==} + '@storybook/addon-a11y@10.2.10': + resolution: {integrity: sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q==} peerDependencies: - storybook: ^10.0.8 + storybook: ^10.2.10 - '@storybook/addon-docs@10.0.8': - resolution: {integrity: sha512-PYuaGXGycsamK/7OrFoE4syHGy22mdqqArl67cfosRwmRxZEI9ManQK0jTjNQM9ZX14NpThMOSWNGoWLckkxog==} + '@storybook/addon-docs@10.2.10': + resolution: {integrity: sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ==} peerDependencies: - storybook: ^10.0.8 + storybook: ^10.2.10 - '@storybook/addon-links@10.0.8': - resolution: {integrity: sha512-LnakruogdN5ND0cF0SOKyhzbEeIGDe1njkufX2aR9LOXQ0mMj5S2P86TdP87dR5R9bJjYYPPg/F7sjsAiI1Lqg==} + '@storybook/addon-links@10.2.10': + resolution: {integrity: sha512-oo9Xx4/2OVJtptXKpqH4ySri7ZuBdiSOXlZVGejEfLa0Jeajlh/KIlREpGvzPPOqUVT7dSddWzBjJmJUyQC3ew==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.2.10 peerDependenciesMeta: react: optional: true - '@storybook/addon-themes@10.0.8': - resolution: {integrity: sha512-rqESiwPfYClMSaeHIYxmAz6CR50dQTMCKa2mO8ZvEikdo1IBtzHJfmD9iAC6QQaFZSMG/17KVIWXJVjzWjLdfQ==} + '@storybook/addon-themes@10.2.10': + resolution: {integrity: sha512-j7ixCgzpWeTU7K4BkNHtEg3NdmRg9YW7ynvv0OjD3vaz4+FUVWOq7PPwb3SktLS1tOl4UA13IpApD8nSpBiY6A==} peerDependencies: - storybook: ^10.0.8 + storybook: ^10.2.10 '@storybook/addon-webpack5-compiler-swc@4.0.2': resolution: {integrity: sha512-I/B4zXnpk+wLs2YA/VcCzUjF/TtB26X4zIoXw3xaPPUvk5aPc76/dhmZHLMXkICQEur5FkFQv0YGHNxWHbhnfw==} @@ -1084,26 +1157,26 @@ packages: peerDependencies: storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 - '@storybook/builder-webpack5@10.0.8': - resolution: {integrity: sha512-uufIrphiv0BoaZ0kE/jk7q3tRs7+hjxnesRceeE3TAUI38EFs3Ppv3UBkwjl1NxL2h7skJhQ45rogtBFLwkyew==} + '@storybook/builder-webpack5@10.2.10': + resolution: {integrity: sha512-bIHAXiX9NwZlB5dJ2W+rZcwo1Dkmg0JOwL/F/rB9O4IlkjTsoOe/+BcLchfRdqRk7ENCVFNwaq8aXxnKmiIOMQ==} peerDependencies: - storybook: ^10.0.8 + storybook: ^10.2.10 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@storybook/core-webpack@10.0.8': - resolution: {integrity: sha512-WFpbzUY9lfXMvNo9YTDG9CeGlWhn79V/pVqj6nOYwCO4wrF+yeizm61zXvZdDofFOtrw4vzzmzgbjkBQJefa5Q==} + '@storybook/core-webpack@10.2.10': + resolution: {integrity: sha512-bhz20jQWn0UB6GfYeO3oou8w8jXSVs+dgPglsxPr+tOusUuyT5FO270PHixZovVtrHgFAKHLXUEHUNuOvUsMig==} peerDependencies: - storybook: ^10.0.8 + storybook: ^10.2.10 - '@storybook/csf-plugin@10.0.8': - resolution: {integrity: sha512-OtLUWHIm3SDGtclQn6Mdd/YsWizLBgdEBRAdekGtwI/TvICfT7gpWYIycP53v2t9ufu2MIXjsxtV2maZKs8sZg==} + '@storybook/csf-plugin@10.2.10': + resolution: {integrity: sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.0.8 + storybook: ^10.2.10 vite: '*' webpack: '*' peerDependenciesMeta: @@ -1119,19 +1192,18 @@ packages: '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - '@storybook/icons@1.6.0': - resolution: {integrity: sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==} - engines: {node: '>=14.0.0'} + '@storybook/icons@2.0.1': + resolution: {integrity: sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/preset-react-webpack@10.0.8': - resolution: {integrity: sha512-RM12Y4WnNN2+dTT1jCLbnrkp+rb4bResYhgi+QY4Yf2hc2Tq9OaApiFTiFaZqO8h4/X4UKgPto6ZY2kMMtlg4w==} + '@storybook/preset-react-webpack@10.2.10': + resolution: {integrity: sha512-DaV7uKpNF/2iBjcGL81HA7Kx8ZZb9D4MfG1VxpdtmDOKS20YIDNdCFeUbcAkUlG3lhshUGcGL8YiRp3o4b1X6Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.2.10 typescript: '*' peerDependenciesMeta: typescript: @@ -1143,30 +1215,30 @@ packages: typescript: '>= 4.x' webpack: '>= 4' - '@storybook/react-dom-shim@10.0.8': - resolution: {integrity: sha512-ojuH22MB9Sz6rWbhTmC5IErZr0ZADbZijtPteUdydezY7scORT00UtbNoBcG0V6iVjdChgDtSKw2KHUUfchKqg==} + '@storybook/react-dom-shim@10.2.10': + resolution: {integrity: sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.2.10 - '@storybook/react-webpack5@10.0.8': - resolution: {integrity: sha512-NvDRRmSPPQ/TYWY1iwsYMDZtUdUYEbOei0FvdKXtqxkmtLonRyG/a7nNOVWk5+rkj2CjQbnTFKNhv4qWGzuVsg==} + '@storybook/react-webpack5@10.2.10': + resolution: {integrity: sha512-TofUD2dRuOgWOS2RzwfBA5/ihyd9I/FiCjcIUvB4xLo/lXVZVgI9SvjfkRQEPO3EDshxTfKDhtCzzzCe28Tk9g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.2.10 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@storybook/react@10.0.8': - resolution: {integrity: sha512-PkuPb8sAqmjjkowSzm3rutiSuETvZI2F8SnjbHE6FRqZWWK4iFoaUrQbrg5kpPAtX//xIrqkdFwlbmQ3skhiPA==} + '@storybook/react@10.2.10': + resolution: {integrity: sha512-PcsChzPI8lhllB9exV7nFb96093i6sTwIl0jpPjaTFPQCRoueR9E/YeP3qSKQL9xt4cmii0cW7F0RUx25rW93Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.2.10 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -1306,6 +1378,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1357,8 +1432,8 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} '@vitest/coverage-v8@3.0.9': resolution: {integrity: sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==} @@ -1386,17 +1461,6 @@ packages: vite: optional: true - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/pretty-format@3.0.9': resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} @@ -1481,11 +1545,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2959,6 +3018,10 @@ packages: resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} engines: {node: '>=18'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -3206,6 +3269,10 @@ packages: resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} engines: {node: '>=16.14.0'} + react-docgen@8.0.2: + resolution: {integrity: sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==} + engines: {node: ^20.9.0 || >=22} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -3392,6 +3459,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3470,8 +3542,8 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} - storybook@10.0.8: - resolution: {integrity: sha512-vQMufKKA9TxgoEDHJv3esrqUkjszuuRiDkThiHxENFPdQawHhm2Dei+iwNRwH5W671zTDy9iRT9P1KDjcU5Iyw==} + storybook@10.2.10: + resolution: {integrity: sha512-N4U42qKgzMHS7DjqLz5bY4P7rnvJtYkWFCyKspZr3FhPUuy6CWOae3aYC2BjXkHrdug0Jyta6VxFTuB1tYUKhg==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -3791,6 +3863,11 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4010,6 +4087,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -4076,8 +4157,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.26.8': {} + '@babel/compat-data@7.29.0': {} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -4098,6 +4187,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.26.10': dependencies: '@babel/parser': 7.26.10 @@ -4106,6 +4215,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.26.5': dependencies: '@babel/compat-data': 7.26.8 @@ -4114,6 +4231,16 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.26.10 @@ -4121,6 +4248,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -4130,21 +4264,45 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-option@7.27.1': {} + '@babel/helpers@7.26.10': dependencies: '@babel/template': 7.26.9 '@babel/types': 7.26.10 + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.26.10': dependencies: '@babel/types': 7.26.10 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 @@ -4155,6 +4313,12 @@ snapshots: '@babel/parser': 7.26.10 '@babel/types': 7.26.10 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.26.10': dependencies: '@babel/code-frame': 7.26.2 @@ -4167,11 +4331,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.26.10': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@biomejs/biome@1.9.4': @@ -4554,6 +4735,11 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -4581,6 +4767,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4835,21 +5026,21 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@storybook/addon-a11y@10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))': + '@storybook/addon-a11y@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/addon-docs@10.0.8(@types/react@18.3.13)(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': + '@storybook/addon-docs@10.2.10(@types/react@18.3.13)(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.13)(react@18.3.1) - '@storybook/csf-plugin': 10.0.8(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) - '@storybook/icons': 1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 10.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + '@storybook/csf-plugin': 10.2.10(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) + '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 10.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -4858,30 +5049,30 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.0.8(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))': + '@storybook/addon-links@10.2.10(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: react: 18.3.1 - '@storybook/addon-themes@10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))': + '@storybook/addon-themes@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 - '@storybook/addon-webpack5-compiler-swc@4.0.2(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': + '@storybook/addon-webpack5-compiler-swc@4.0.2(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': dependencies: '@swc/core': 1.15.3 - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) swc-loader: 0.2.6(@swc/core@1.15.3)(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) transitivePeerDependencies: - '@swc/helpers' - webpack - '@storybook/builder-webpack5@10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2)': + '@storybook/builder-webpack5@10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2)': dependencies: - '@storybook/core-webpack': 10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + '@storybook/core-webpack': 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 css-loader: 7.1.2(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) @@ -4889,7 +5080,7 @@ snapshots: fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.2)(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) html-webpack-plugin: 5.6.3(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) magic-string: 0.30.17 - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) style-loader: 4.0.0(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) terser-webpack-plugin: 5.3.14(@swc/core@1.15.3)(esbuild@0.25.0)(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) ts-dedent: 2.2.0 @@ -4906,14 +5097,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/core-webpack@10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))': + '@storybook/core-webpack@10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 - '@storybook/csf-plugin@10.0.8(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': + '@storybook/csf-plugin@10.2.10(esbuild@0.25.0)(rollup@4.36.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0))': dependencies: - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) unplugin: 2.3.10 optionalDependencies: esbuild: 0.25.0 @@ -4923,23 +5114,23 @@ snapshots: '@storybook/global@5.0.0': {} - '@storybook/icons@1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/icons@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/preset-react-webpack@10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2)': + '@storybook/preset-react-webpack@10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2)': dependencies: - '@storybook/core-webpack': 10.0.8(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + '@storybook/core-webpack': 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.2)(webpack@5.98.0(@swc/core@1.15.3)(esbuild@0.25.0)) - '@types/semver': 7.5.8 + '@types/semver': 7.7.1 magic-string: 0.30.17 react: 18.3.1 react-docgen: 7.1.1 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - semver: 7.7.1 - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + semver: 7.7.4 + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tsconfig-paths: 4.2.0 webpack: 5.98.0(@swc/core@1.15.3)(esbuild@0.25.0) optionalDependencies: @@ -4965,20 +5156,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@10.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))': + '@storybook/react-dom-shim@10.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-webpack5@10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2)': + '@storybook/react-webpack5@10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2)': dependencies: - '@storybook/builder-webpack5': 10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2) - '@storybook/preset-react-webpack': 10.0.8(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2) - '@storybook/react': 10.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2) + '@storybook/builder-webpack5': 10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2) + '@storybook/preset-react-webpack': 10.2.10(@swc/core@1.15.3)(esbuild@0.25.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2) + '@storybook/react': 10.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -4989,15 +5180,18 @@ snapshots: - uglify-js - webpack-cli - '@storybook/react@10.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)))(typescript@5.8.2)': + '@storybook/react@10.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))) + '@storybook/react-dom-shim': 10.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: 18.3.1 + react-docgen: 8.0.2 react-dom: 18.3.1(react@18.3.1) - storybook: 10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) + storybook: 10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: typescript: 5.8.2 + transitivePeerDependencies: + - supports-color '@swc/core-darwin-arm64@1.15.3': optional: true @@ -5101,24 +5295,28 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/types': 7.29.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/types': 7.29.0 '@types/babel__traverse@7.20.6': dependencies: '@babel/types': 7.26.10 + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5174,7 +5372,7 @@ snapshots: '@types/resolve@1.20.6': {} - '@types/semver@7.5.8': {} + '@types/semver@7.7.1': {} '@vitest/coverage-v8@3.0.9(vitest@3.0.9(@types/node@22.13.11)(jiti@2.4.1)(jsdom@26.0.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))': dependencies: @@ -5217,14 +5415,6 @@ snapshots: optionalDependencies: vite: 6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1) - '@vitest/mocker@3.2.4(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1) - '@vitest/pretty-format@3.0.9': dependencies: tinyrainbow: 2.0.0 @@ -5348,8 +5538,6 @@ snapshots: acorn@8.11.3: {} - acorn@8.14.1: {} - acorn@8.15.0: {} agent-base@7.1.3: {} @@ -6805,6 +6993,13 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + ora@5.4.1: dependencies: bl: 4.1.0 @@ -7071,6 +7266,21 @@ snapshots: transitivePeerDependencies: - supports-color + react-docgen@8.0.2: + dependencies: + '@babel/core': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.10 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -7313,6 +7523,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.4: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -7377,27 +7589,26 @@ snapshots: stdin-discarder@0.2.2: {} - storybook@10.0.8(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)): + storybook@10.2.10(@testing-library/dom@10.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@storybook/global': 5.0.0 - '@storybook/icons': 1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/jest-dom': 6.6.3 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.2.2(@types/node@22.13.11)(jiti@2.4.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.6.1)) '@vitest/spy': 3.2.4 esbuild: 0.25.1 + open: 10.2.0 recast: 0.23.11 - semver: 7.7.1 + semver: 7.7.4 + use-sync-external-store: 1.6.0(react@18.3.1) ws: 8.18.1 transitivePeerDependencies: - '@testing-library/dom' - bufferutil - - msw - react - react-dom - utf-8-validate - - vite string-width@4.2.3: dependencies: @@ -7508,7 +7719,7 @@ snapshots: terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -7698,6 +7909,10 @@ snapshots: url-join@5.0.0: {} + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utila@0.4.0: {} @@ -7821,7 +8036,7 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 + acorn: 8.15.0 browserslist: 4.24.4 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 @@ -7918,6 +8133,10 @@ snapshots: ws@8.18.1: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + xdg-basedir@5.1.0: {} xml-name-validator@5.0.0: {} diff --git a/src/Odontogram.tsx b/src/Odontogram.tsx index 26dfa23..929a5dd 100644 --- a/src/Odontogram.tsx +++ b/src/Odontogram.tsx @@ -2,14 +2,23 @@ import "./styles.css"; import { type CSSProperties, type FC, + type FocusEvent, + type KeyboardEvent, + type MouseEvent, useCallback, useRef, useState, } from "react"; +import { Teeth } from "./Teeth"; import { OdontogramTooltip } from "./Tooltip"; import { teethPaths } from "./data"; -import type { OdontogramProps, Placement, ToothDetail } from "./type"; -import { Teeth } from "./Teeth"; +import type { + Notation, + OdontogramColors, + OdontogramProps, + Placement, + ToothDetail, +} from "./type"; const placements: Record< Placement, @@ -29,10 +38,53 @@ const placements: Record< "right-end": (t, m) => ({ x: t.right + m, y: t.bottom }), }; -export function convertFDIToNotation( - fdi: string, - notation: "FDI" | "Universal" | "Palmer" -) { +const quadrants: Array<{ + name: "first" | "second" | "third" | "fourth"; + transform: string; + label: string; +}> = [ + { + name: "first", + transform: "", + label: "Upper Right", + }, + { + name: "second", + transform: "scale(-1, 1) translate(-409, 0)", + label: "Upper Left", + }, + { + name: "third", + transform: "scale(1, -1) translate(0, -694)", + label: "Lower Right", + }, + { + name: "fourth", + transform: "scale(-1, -1) translate(-409, -694)", + label: "Lower Left", + }, +]; + +const toothTypeByName = new Map(teethPaths.map((tooth) => [tooth.name, tooth.type])); + +const getToothType = (id: string) => { + const toothName = id.replace("teeth-", "").slice(1); + return toothTypeByName.get(toothName) ?? "Unknown"; +}; + +const clampMaxTeeth = (maxTeeth: number | undefined) => { + if (typeof maxTeeth !== "number" || Number.isNaN(maxTeeth)) { + return teethPaths.length; + } + + return Math.max(0, Math.min(teethPaths.length, Math.floor(maxTeeth))); +}; + +type ToothInteractionEvent = + | MouseEvent + | FocusEvent; + +export function convertFDIToNotation(fdi: string, notation: Notation) { const num = fdi.replace("teeth-", ""); const fdiToUniversal: Record = { @@ -75,14 +127,19 @@ export function convertFDIToNotation( } if (notation === "Palmer") { + if (num.length < 2) { + return num; + } + const quadrant = num[0]; const tooth = num[1]; const symbols: Record = { - "1": "UR", // upper right - "2": "UL", // upper left - "3": "LL", // lower left - "4": "LR", // lower right + "1": "UR", + "2": "UL", + "3": "LL", + "4": "LR", }; + return `${tooth}${symbols[quadrant] ?? ""}`; } @@ -93,6 +150,7 @@ export function getToothNotations(fdi: string) { const num = fdi.replace("teeth-", ""); const universal = convertFDIToNotation(fdi, "Universal"); const palmer = convertFDIToNotation(fdi, "Palmer"); + return { fdi: num, universal, @@ -100,43 +158,46 @@ export function getToothNotations(fdi: string) { }; } +const buildToothDetail = (id: string): ToothDetail => ({ + id, + notations: getToothNotations(id), + type: getToothType(id), +}); + export const Odontogram: FC = ({ defaultSelected = [], onChange, className = "", theme = "light", colors = {}, - notation, + notation = "FDI", tooltip = { - margin: -10, - placement: "top" + margin: 10, + placement: "top", }, showTooltip = true, showHalf = "full", name, - maxTeeth = 8, - + maxTeeth = teethPaths.length, }) => { const themeColors = theme === "dark" ? { - "--dark-blue": "#aab6ff", - "--base-blue": "#d0d5f6", - "--light-blue": "#5361e6", - } + "--dark-blue": "#aab6ff", + "--base-blue": "#d0d5f6", + "--light-blue": "#5361e6", + } : { - "--dark-blue": "#3e5edc", - "--base-blue": "#8a98be", - "--light-blue": "#c6ccf8", - }; + "--dark-blue": "#3e5edc", + "--base-blue": "#8a98be", + "--light-blue": "#c6ccf8", + }; const [selected, setSelected] = useState>( - new Set(defaultSelected) + () => new Set(defaultSelected) ); const svgRef = useRef(null); - const _tooltipRef = useRef(null); - const [tooltipData, setTooltipData] = useState<{ active: boolean; position?: { x: number; y: number }; @@ -144,130 +205,78 @@ export const Odontogram: FC = ({ }>({ active: false }); const handleToggle = useCallback( - (name: string) => { - setSelected((prev) => { - const updated = new Set(prev); - updated.has(name) ? updated.delete(name) : updated.add(name); - - // build detailed JSON output - const details = Array.from(updated).map((id) => { - const fdi = id.replace("teeth-", ""); - const toothBase = fdi.slice(1); - const toothData = teethPaths.find((t) => t.name === toothBase); - return { - id, - notations: getToothNotations(id), - type: toothData?.type ?? "Unknown", - }; - }); - - onChange?.(details); + (id: string) => { + setSelected((previous) => { + const updated = new Set(previous); + if (updated.has(id)) { + updated.delete(id); + } else { + updated.add(id); + } + + onChange?.(Array.from(updated).map(buildToothDetail)); return updated; }); }, [onChange] ); + const handleKeyDown = useCallback( - (e: React.KeyboardEvent, name: string) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleToggle(name); + (event: KeyboardEvent, id: string) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleToggle(id); } }, [handleToggle] ); - const quadrants: Array<{ - name: "first" | "second" | "third" | "fourth"; - transform: string; - label: string; - position: { x: number; y: number }; - }> = [ - { - name: "first", - transform: "", - label: "Upper Right", - position: { x: 100, y: 30 }, - }, - { - name: "second", - transform: "scale(-1, 1) translate(-409, 0)", - label: "Upper Left", - position: { x: 309, y: 30 }, - }, - { - name: "third", - transform: "scale(1, -1) translate(0, -694)", - label: "Lower Right", - position: { x: 100, y: 664 }, - }, - { - name: "fourth", - transform: "scale(-1, -1) translate(-409, -694)", - label: "Lower Left", - position: { x: 309, y: 664 }, - }, - ]; - - let visibleQuadrants = quadrants; - if (showHalf === "upper") { - visibleQuadrants = quadrants.slice(0, 2); - } else if (showHalf === "lower") { - visibleQuadrants = quadrants.slice(2); - } - - const handleHover = ( - name: string, - e: React.MouseEvent, - placement: Placement = "right" // default - ) => { - const target = e.currentTarget as SVGGElement; - const path = target.querySelector("path"); - - if (!(path && svgRef.current)) { - return; - } + const handleHover = useCallback( + ( + id: string, + event: ToothInteractionEvent, + placement: Placement = "right" + ) => { + const path = event.currentTarget.querySelector("path"); + if (!(path && svgRef.current)) { + return; + } - const toothBox = path.getBoundingClientRect(); - const svgBox = svgRef.current.getBoundingClientRect(); + const toothBox = path.getBoundingClientRect(); + const svgBox = svgRef.current.getBoundingClientRect(); + const margin = tooltip.margin ?? 10; - const margin = tooltip?.margin || 10; // distance between tooth and tooltip + const { x, y } = + placements[placement]?.(toothBox, margin) ?? + placements.right(toothBox, margin); + const safeY = y < svgBox.top ? toothBox.bottom + margin : y; - // Compute tooltip position just above or below depending on space + setTooltipData({ + active: true, + position: { x, y: safeY }, + payload: buildToothDetail(id), + }); + }, + [tooltip.margin] + ); - console.log( - "toothbox box", toothBox, - "svgBOx", svgBox - ) + const handleLeave = useCallback(() => { + setTooltipData((previous) => ({ ...previous, active: false })); + }, []); - const { x, y } = - placements[placement]?.(toothBox, margin) ?? - placements.right(toothBox, margin); + const visibleQuadrants = + showHalf === "upper" + ? quadrants.slice(0, 2) + : showHalf === "lower" + ? quadrants.slice(2) + : quadrants; - const safeY = y < svgBox.top ? toothBox.bottom + margin : y; + const filteredTeeth = teethPaths.slice(0, clampMaxTeeth(maxTeeth)); - setTooltipData({ - active: true, - position: { x, y: safeY }, - payload: { - id: name, - notations: getToothNotations(name), - type: - teethPaths.find((t) => t.name === name.replace("teeth-", "").slice(1)) - ?.type ?? "Unknown", - }, - }); - }; - const handleLeave = () => setTooltipData((p) => ({ ...p, active: false })); - const renderTeeth = (prefix: string, maxTeeth?: number) => { - // FIXED: use slice() instead of splice() - const filteredTeeth = maxTeeth - ? teethPaths.slice(0, maxTeeth) - : teethPaths; - - return filteredTeeth.map((tooth) => { + const renderTeeth = (prefix: string) => + filteredTeeth.map((tooth) => { const id = `${prefix}${tooth.name}`; - const displayName = convertFDIToNotation(id, notation ?? "FDI"); + const displayName = convertFDIToNotation(id, notation); return ( = ({ selected={selected.has(id)} onClick={handleToggle} onKeyDown={handleKeyDown} - onHover={(name, e) => handleHover(name, e, tooltip?.placement)} - onLeave={handleLeave} + onHover={ + showTooltip + ? (currentId, event) => + handleHover(currentId, event, tooltip.placement) + : undefined + } + onFocus={ + showTooltip + ? (currentId, event) => + handleHover(currentId, event, tooltip.placement) + : undefined + } + onLeave={showTooltip ? handleLeave : undefined} + onBlur={showTooltip ? handleLeave : undefined} > {displayName} ); }); + + const finalColors = { + ...themeColors, + ...mapToCssVars(colors), }; - const finalColors = { ...themeColors, ...mapToCssVars(colors) }; + const containerClasses = ["Odontogram", theme === "dark" ? "dark-theme" : "", className] + .filter(Boolean) + .join(" "); return (
= ({ display: "flex", justifyContent: "center", alignItems: "center", - // isolation: "isolate", }} role="listbox" aria-label="Odontogram" @@ -318,8 +344,8 @@ export const Odontogram: FC = ({ showHalf === "full" ? "0 0 409 694" : showHalf === "upper" - ? "0 0 409 347" - : "0 347 409 347" + ? "0 0 409 347" + : "0 347 409 347" } className="Odontogram" style={{ @@ -330,15 +356,14 @@ export const Odontogram: FC = ({ }} > Odontogram - {visibleQuadrants.map(({ name, transform, label, position }, index) => ( + {visibleQuadrants.map(({ name, transform, label }, index) => ( - {renderTeeth(`teeth-${index + 1}`, maxTeeth)} + {renderTeeth(`teeth-${index + 1}`)} ))} @@ -347,15 +372,16 @@ export const Odontogram: FC = ({ active={tooltipData.active} position={tooltipData.position} payload={tooltipData.payload} - content={tooltip?.content} + content={tooltip.content} /> )}
); }; -export function mapToCssVars(colors: Record) { +export function mapToCssVars(colors: OdontogramColors) { const cssVars: Record = {}; + if (colors.darkBlue) { cssVars["--dark-blue"] = colors.darkBlue; } @@ -365,6 +391,7 @@ export function mapToCssVars(colors: Record) { if (colors.lightBlue) { cssVars["--light-blue"] = colors.lightBlue; } + return cssVars; } diff --git a/src/Teeth.tsx b/src/Teeth.tsx index a31aa5a..6688239 100644 --- a/src/Teeth.tsx +++ b/src/Teeth.tsx @@ -1,4 +1,4 @@ -import { TeethProps } from "./type"; +import type { TeethProps } from "./type"; export const Teeth = ({ name, @@ -9,7 +9,9 @@ export const Teeth = ({ onClick, onKeyDown, onHover, + onFocus, onLeave, + onBlur, children, }: TeethProps) => ( onClick?.(name)} onKeyDown={(e) => onKeyDown?.(e, name)} - onMouseMove={(e) => onHover?.(name, e)} + onMouseEnter={(e) => onHover?.(name, e)} + onFocus={(e) => onFocus?.(name, e)} onMouseLeave={onLeave} + onBlur={onBlur} role="option" aria-selected={selected} - aria-label={`Tooth ${name}`} + aria-label={`Tooth ${name.replace("teeth-", "")}`} style={{ cursor: "pointer", outline: "none", diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index f8d45e3..12b89f2 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -1,44 +1,64 @@ -import { useEffect, useRef, useState } from "react"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import type { ToothDetail, TooltipContentRenderer } from "./type"; -export type TooltipContentRenderer = (payload?: any) => React.ReactNode; +const ARROW_OFFSET = 12; +const VIEWPORT_PADDING = 8; export interface OdontogramTooltipProps { active: boolean; - payload?: any; + payload?: ToothDetail; position?: { x: number; y: number }; - content?: React.ReactNode | TooltipContentRenderer; + content?: ReactNode | TooltipContentRenderer; } +const clamp = (value: number, min: number, max: number) => + Math.min(Math.max(value, min), max); + const getContent = ( - content: React.ReactNode | TooltipContentRenderer, - payload?: any + content: ReactNode | TooltipContentRenderer | undefined, + payload?: ToothDetail ) => { + if (!content) { + return undefined; + } + return typeof content === "function" ? content(payload) : content; }; -export const OdontogramTooltip: React.FC = ({ +export function OdontogramTooltip({ active, payload, position, content, -}) => { +}: OdontogramTooltipProps) { const ref = useRef(null); - const [coords, setCoords] = useState({ left: 0, top: 0 }); + const [coords, setCoords] = useState<{ left: number; top: number } | null>( + null + ); useEffect(() => { - if (!ref.current || !position) return; + if (!(active && ref.current && position)) { + return; + } const tooltipBox = ref.current.getBoundingClientRect(); const { x, y } = position; - // place tooltip above the hover point - const left = x - tooltipBox.width / 2; - const top = y - tooltipBox.height - 12; // space for arrow + const maxLeft = Math.max( + VIEWPORT_PADDING, + window.innerWidth - tooltipBox.width - VIEWPORT_PADDING + ); + const left = clamp(x - tooltipBox.width / 2, VIEWPORT_PADDING, maxLeft); + + const nextTop = y - tooltipBox.height - ARROW_OFFSET; + const top = nextTop < VIEWPORT_PADDING ? y + ARROW_OFFSET : nextTop; setCoords({ left, top }); - }, [position, content, payload]); + }, [active, position, content, payload]); - if (!(active && payload)) return null; + if (!(active && payload)) { + return null; + } return (
= ({ style={{ position: "fixed", pointerEvents: "none", - background: "rgba(0,0,0,0.85)", - color: "#fff", + background: "var(--odontogram-tooltip-bg, rgba(0,0,0,0.85))", + color: "var(--odontogram-tooltip-fg, #fff)", padding: "6px 10px", borderRadius: "6px", fontSize: "12px", lineHeight: 1.3, whiteSpace: "nowrap", zIndex: 1000, - - left: coords.left, - top: coords.top, - - opacity: active ? 1 : 0, + left: coords?.left ?? -9999, + top: coords?.top ?? -9999, + opacity: coords ? 1 : 0, transition: "opacity 0.15s ease", }} > - {/* tooltip content */} {getContent(content, payload) ?? ( <> -
Tooth: {payload?.notations?.fdi}
-
Type: {payload?.type}
+
Tooth: {payload.notations.fdi}
+
Type: {payload.type}
- Universal: {payload?.notations?.universal}, Palmer:{" "} - {payload?.notations?.palmer} + Universal: {payload.notations.universal}, Palmer:{" "} + {payload.notations.palmer}
)} - - {/* ARROW */}
= ({ height: 0, borderLeft: "6px solid transparent", borderRight: "6px solid transparent", - borderTop: "6px solid rgba(0, 0, 0, 0.85)", // arrow color + borderTop: + "6px solid var(--odontogram-tooltip-bg, rgba(0, 0, 0, 0.85))", }} />
); -}; +} diff --git a/src/index.ts b/src/index.ts index cd47273..d2d0ede 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export { default } from "./Odontogram"; export * from "./Odontogram"; +export type * from "./type"; diff --git a/src/stories/Form.stories.tsx b/src/stories/Form.stories.tsx index e343faa..6f4c2b9 100644 --- a/src/stories/Form.stories.tsx +++ b/src/stories/Form.stories.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import Odontogram, { getToothNotations } from ".."; -import { ToothDetail } from "../type"; +import type { ToothDetail } from "../type"; import { teethPaths } from "../data"; export default { @@ -42,7 +42,8 @@ const Template: StoryFn = (args) => { id, notations: getToothNotations(id), type: - teethPaths.find((t) => t.name === id.replace("teeth-", ""))?.type ?? + teethPaths.find((t) => t.name === id.replace("teeth-", "").slice(1)) + ?.type ?? "Unknown", })) ?? [] ); diff --git a/src/stories/zBaby.stories.tsx b/src/stories/zBaby.stories.tsx index 8d4db17..ed07485 100644 --- a/src/stories/zBaby.stories.tsx +++ b/src/stories/zBaby.stories.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import Odontogram, { getToothNotations } from ".."; -import { ToothDetail } from "../type"; +import type { ToothDetail } from "../type"; import { teethPaths } from "../data"; export default { @@ -46,7 +46,8 @@ const Template: StoryFn = (args) => { id, notations: getToothNotations(id), type: - teethPaths.find((t) => t.name === id.replace("teeth-", ""))?.type ?? + teethPaths.find((t) => t.name === id.replace("teeth-", "").slice(1)) + ?.type ?? "Unknown", })) ?? [] ); diff --git a/src/styles.css b/src/styles.css index a7d5003..e2b1bae 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,108 +1,100 @@ :root { - --dark-blue: #3e5edc; - --base-blue: #8a98be; - --light-blue: #c6ccf8; + --dark-blue: #3e5edc; + --base-blue: #8a98be; + --light-blue: #c6ccf8; } - - #storybook-root { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } .dark-template { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background-color: #0b0d1a; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: #0b0d1a; +} + +.Odontogram { + --odontogram-tooltip-bg: rgba(0, 0, 0, 0.85); + --odontogram-tooltip-fg: #fff; } -/* 🌙 Dark theme overrides */ -.Odontogram svg.dark-theme { - --dark-blue: #aab6ff; /* lighter blue for contrast on dark bg */ - --base-blue: #d0d5f6; /* light muted base */ - --light-blue: #5361e6; /* vibrant highlight blue */ - background-color: #0b0d1a; /* optional page background */ - color-scheme: dark; - color: #e4e6ef; +.Odontogram.dark-theme { + --odontogram-tooltip-bg: rgba(255, 255, 255, 0.95); + --odontogram-tooltip-fg: #000; +} + +.Odontogram.dark-theme svg { + --dark-blue: #aab6ff; + --base-blue: #d0d5f6; + --light-blue: #5361e6; + background-color: #0b0d1a; + color-scheme: dark; + color: #e4e6ef; } .Odontogram svg { - color: var(--base-blue); - fill: "none"; - transition: all ease-in 125ms; + color: var(--base-blue); + fill: none; + transition: all ease-in 125ms; } .Odontogram svg path:nth-of-type(2) { - opacity: 0; - transition: all ease-in 200ms; + opacity: 0; + transition: all ease-in 200ms; } .Odontogram g.selected path:nth-of-type(2) { - fill: var(--light-blue); - opacity: 1; + fill: var(--light-blue); + opacity: 1; } .Odontogram g.selected path:first-of-type { - transition: stroke 1s ease; + transition: stroke 1s ease; } -/* Animate dash on hover */ .Odontogram g.selected:hover path:first-of-type { - stroke: currentColor; - stroke-width: 1; - stroke-linecap: round; - stroke-dasharray: 4 4; /* dash length + gap */ - stroke-dashoffset: 0; - transition: stroke 5s ease; - animation: dash-move 1s linear infinite; - animation-delay: 1s; - - filter: drop-shadow(0 0 8px currentColor); - -webkit-filter: drop-shadow(0 0 8px currentColor); + stroke: currentColor; + stroke-width: 1; + stroke-linecap: round; + stroke-dasharray: 4 4; + stroke-dashoffset: 0; + transition: stroke 5s ease; + animation: dash-move 1s linear infinite; + animation-delay: 1s; + filter: drop-shadow(0 0 8px currentColor); + -webkit-filter: drop-shadow(0 0 8px currentColor); } @keyframes dash-move { - to { - stroke-dashoffset: 8; /* move by one dash+gap length */ - } + to { + stroke-dashoffset: 8; + } } .Odontogram g.selected { - /* fill: var(--light-blue); */ - /* stroke: var(--dark-blue); */ - /* stroke-width: 1.5; */ - transition: all 0.3s ease; - color: var(--dark-blue); + transition: all 0.3s ease; + color: var(--dark-blue); } .Odontogram g.selected path { - stroke-width: 1.5; + stroke-width: 1.5; } -g[class^="teeth-"]:hover path:nth-of-type(2) { - fill: var(--light-blue); - opacity: 1; +.Odontogram g[class^="teeth-"]:hover path:nth-of-type(2) { + fill: var(--light-blue); + opacity: 1; } .odontogram-tooltip { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(4px); } -.dark-theme .odontogram-tooltip { - background: rgba(255, 255, 255, 0.95); - color: #000; -} - - - - .Odontogram g[role="option"]:focus-visible { -outline: 4px solid var(--dark-blue) !important; + outline: 4px solid var(--dark-blue) !important; } - - diff --git a/src/type.ts b/src/type.ts index 46162eb..0a1d774 100644 --- a/src/type.ts +++ b/src/type.ts @@ -1,4 +1,13 @@ -import { type KeyboardEvent, type MouseEvent, type ReactNode } from "react"; +import type { + FocusEvent, + KeyboardEvent, + MouseEvent, + ReactNode, +} from "react"; + +export type Notation = "FDI" | "Universal" | "Palmer"; + +export type Theme = "light" | "dark"; export type Placement = | "top" @@ -24,6 +33,14 @@ export interface ToothDetail { type: string; } +export interface OdontogramColors { + darkBlue?: string; + baseBlue?: string; + lightBlue?: string; +} + +export type TooltipContentRenderer = (payload?: ToothDetail) => ReactNode; + export interface TeethProps { name: string; outlinePath: string; @@ -33,8 +50,10 @@ export interface TeethProps { onClick?: (name: string) => void; onKeyDown?: (e: KeyboardEvent, name: string) => void; children?: ReactNode; - onHover?: (name: string, event: MouseEvent, placement?: Placement) => void; + onHover?: (name: string, event: MouseEvent) => void; + onFocus?: (name: string, event: FocusEvent) => void; onLeave?: () => void; + onBlur?: () => void; } export interface OdontogramProps { @@ -44,16 +63,15 @@ export interface OdontogramProps { className?: string; selectedColor?: string; hoverColor?: string; - theme: "light" | "dark"; - colors: Record; - notation?: "FDI" | "Universal" | "Palmer"; + theme?: Theme; + colors?: OdontogramColors; + notation?: Notation; tooltip?: { placement?: Placement; margin?: number; - content?: React.ReactNode | ((payload?: ToothDetail) => React.ReactNode); + content?: ReactNode | TooltipContentRenderer; }; showTooltip?: boolean; showHalf?: "upper" | "lower" | "full"; maxTeeth?: number; - } diff --git a/tests/Odontogram.test.tsx b/tests/Odontogram.test.tsx index 1007610..980e80c 100644 --- a/tests/Odontogram.test.tsx +++ b/tests/Odontogram.test.tsx @@ -1,69 +1,111 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import Odontogram from "../src/Odontogram"; - -describe("Odontogram Component Snapshots", () => { - it("renders full odontogram correctly", () => { - const { container } = render( - , - ); - - expect(container).toMatchSnapshot(); - }); - - it("renders upper half odontogram", () => { - const { container } = render( - , - ); - - expect(container).toMatchSnapshot(); - }); - - it("renders lower half odontogram", () => { - const { container } = render( - , - ); - - expect(container).toMatchSnapshot(); - }); - - it("renders dark theme correctly", () => { - const { container } = render( - , - ); - - expect(container).toMatchSnapshot(); - }); +import "@testing-library/jest-dom/vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import Odontogram, { + convertFDIToNotation, + getToothNotations, + mapToCssVars, +} from "../src/Odontogram"; + +describe("Odontogram", () => { + it("renders all permanent teeth by default", () => { + render(); + + expect( + screen.getByRole("listbox", { name: "Odontogram" }) + ).toBeInTheDocument(); + expect(screen.getAllByRole("option")).toHaveLength(32); + }); + + it("renders only upper or lower halves when requested", () => { + const { rerender } = render(); + expect(screen.getAllByRole("option")).toHaveLength(16); + + rerender(); + expect(screen.getAllByRole("option")).toHaveLength(16); + }); + + it("respects maxTeeth for each quadrant", () => { + render(); + + expect(screen.getAllByRole("option")).toHaveLength(20); + }); + + it("toggles selection and emits detailed onChange payload", () => { + const onChange = vi.fn(); + const { container } = render(); + const tooth = screen.getByLabelText("Tooth 11"); + + fireEvent.click(tooth); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith([ + { + id: "teeth-11", + notations: { + fdi: "11", + universal: "8", + palmer: "1UR", + }, + type: "Central Incisor", + }, + ]); + + const hiddenInput = container.querySelector( + "input[type='hidden'][name='teeth']" + ); + expect(hiddenInput?.value).toBe(JSON.stringify(["teeth-11"])); + + fireEvent.click(tooth); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange).toHaveBeenLastCalledWith([]); + }); + + it("supports keyboard selection with Enter and Space", () => { + render(); + const tooth = screen.getByLabelText("Tooth 12"); + + fireEvent.keyDown(tooth, { key: "Enter" }); + expect(tooth).toHaveAttribute("aria-selected", "true"); + + fireEvent.keyDown(tooth, { key: " " }); + expect(tooth).toHaveAttribute("aria-selected", "false"); + }); + + it("does not render tooltip when showTooltip is false", () => { + render(); + const tooth = screen.getByLabelText("Tooth 11"); + + fireEvent.mouseEnter(tooth); + + expect(screen.queryByText(/Universal:/)).not.toBeInTheDocument(); + }); +}); + +describe("notation helpers", () => { + it("converts FDI notation to Universal and Palmer", () => { + expect(convertFDIToNotation("teeth-21", "Universal")).toBe("9"); + expect(convertFDIToNotation("teeth-21", "Palmer")).toBe("1UL"); + expect(convertFDIToNotation("teeth-21", "FDI")).toBe("21"); + }); + + it("returns complete notation object", () => { + expect(getToothNotations("teeth-48")).toEqual({ + fdi: "48", + universal: "32", + palmer: "8LR", + }); + }); + + it("maps provided colors to css variables", () => { + expect( + mapToCssVars({ + darkBlue: "#1", + baseBlue: "#2", + }) + ).toEqual({ + "--dark-blue": "#1", + "--base-blue": "#2", + }); + }); }); diff --git a/tests/__snapshots__/Odontogram.test.tsx.snap b/tests/__snapshots__/Odontogram.test.tsx.snap deleted file mode 100644 index ad872bd..0000000 --- a/tests/__snapshots__/Odontogram.test.tsx.snap +++ /dev/null @@ -1,3105 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Odontogram Component Snapshots > renders dark theme correctly 1`] = ` -
-
- - - - Odontogram - - - - - 11 - - - - - - - - 12 - - - - - - - - 13 - - - - - - - - - 14 - - - - - - - - - 15 - - - - - - - - - 16 - - - - - - - - 17 - - - - - - - - 18 - - - - - - - - - - 21 - - - - - - - - 22 - - - - - - - - 23 - - - - - - - - - 24 - - - - - - - - - 25 - - - - - - - - - 26 - - - - - - - - 27 - - - - - - - - 28 - - - - - - - - - - 31 - - - - - - - - 32 - - - - - - - - 33 - - - - - - - - - 34 - - - - - - - - - 35 - - - - - - - - - 36 - - - - - - - - 37 - - - - - - - - 38 - - - - - - - - - - 41 - - - - - - - - 42 - - - - - - - - 43 - - - - - - - - - 44 - - - - - - - - - 45 - - - - - - - - - 46 - - - - - - - - 47 - - - - - - - - 48 - - - - - - - -
-
-`; - -exports[`Odontogram Component Snapshots > renders full odontogram correctly 1`] = ` -
-
- - - - Odontogram - - - - - 11 - - - - - - - - 12 - - - - - - - - 13 - - - - - - - - - 14 - - - - - - - - - 15 - - - - - - - - - 16 - - - - - - - - 17 - - - - - - - - 18 - - - - - - - - - - 21 - - - - - - - - 22 - - - - - - - - 23 - - - - - - - - - 24 - - - - - - - - - 25 - - - - - - - - - 26 - - - - - - - - 27 - - - - - - - - 28 - - - - - - - - - - 31 - - - - - - - - 32 - - - - - - - - 33 - - - - - - - - - 34 - - - - - - - - - 35 - - - - - - - - - 36 - - - - - - - - 37 - - - - - - - - 38 - - - - - - - - - - 41 - - - - - - - - 42 - - - - - - - - 43 - - - - - - - - - 44 - - - - - - - - - 45 - - - - - - - - - 46 - - - - - - - - 47 - - - - - - - - 48 - - - - - - - -
-
-`; - -exports[`Odontogram Component Snapshots > renders lower half odontogram 1`] = ` -
-
- - - - Odontogram - - - - - 11 - - - - - - - - 12 - - - - - - - - 13 - - - - - - - - - 14 - - - - - - - - - 15 - - - - - - - - - 16 - - - - - - - - 17 - - - - - - - - 18 - - - - - - - - - - 21 - - - - - - - - 22 - - - - - - - - 23 - - - - - - - - - 24 - - - - - - - - - 25 - - - - - - - - - 26 - - - - - - - - 27 - - - - - - - - 28 - - - - - - - -
-
-`; - -exports[`Odontogram Component Snapshots > renders upper half odontogram 1`] = ` -
-
- - - - Odontogram - - - - - 11 - - - - - - - - 12 - - - - - - - - 13 - - - - - - - - - 14 - - - - - - - - - 15 - - - - - - - - - 16 - - - - - - - - 17 - - - - - - - - 18 - - - - - - - - - - 21 - - - - - - - - 22 - - - - - - - - 23 - - - - - - - - - 24 - - - - - - - - - 25 - - - - - - - - - 26 - - - - - - - - 27 - - - - - - - - 28 - - - - - - - -
-
-`; diff --git a/tests/setup.ts b/tests/setup.ts index e4458b6..38422b6 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,4 +1,5 @@ import "dotenv/config"; +import "@testing-library/jest-dom/vitest"; import * as matchers from "@testing-library/jest-dom/matchers"; import { cleanup } from "@testing-library/react"; import { afterEach, expect } from "vitest"; diff --git a/todo.md b/todo.md index c828318..2508a15 100644 --- a/todo.md +++ b/todo.md @@ -17,5 +17,79 @@ https://www.a11y-collective.com/blog/svg-accessibility/ https://fizz.studio/blog/reliable-valid-svg-accessibility/ + +### New Color: + + - yellowing - + - fill - #ffeebe + - color - #ffb10a + - bleeding - + - fill - #FFC4C4 + - color - #A50000 +- fracture: + fill: #e3e3e3 + color: #4A4A4A + +- sensitivity: + fill: #c0e5f9 + color: #0077A3 +- plaque: + fill: #fbd5b7 + color: #682d00 + + --- + + + +1. Teal (Plaque / Hygiene-related) +--dark-teal: #1f6f6b; +--base-teal: #6fa7a3; +--light-teal: #cbe5e3; + +1. Green (Healthy / Mild issues) +--dark-green: #2f6b3f; +--base-green: #7fb48d; +--light-green: #d5eadb; + +1. Yellow (Discoloration / Early warning) +--dark-yellow: #b38b00; +--base-yellow: #e0c15a; +--light-yellow: #f5ebc7; + +1. Orange (Inflammation / Moderate risk) +--dark-orange: #c45a1a; +--base-orange: #e29b6c; +--light-orange: #f7d9c6; + +1. Red (Bleeding / Severe conditions) +--dark-red: #9f2f2f; +--base-red: #d27a7a; +--light-red: #f2cccc; + +1. Purple (Suppuration / Infection) +--dark-purple: #5a3a7a; +--base-purple: #9c84b6; +--light-purple: #e2d9ef; + +1. Brown (Calculus / Tartar) +--dark-brown: #6b4a2d; +--base-brown: #a98a6d; +--light-brown: #e6d9cc; + +1. Gray (Missing / Non-vital / Neutral) +--dark-gray: #4b4f5a; +--base-gray: #8c90a0; +--light-gray: #d7dae3; + +1. Pink (Gum sensitivity / Mild bleeding) +--dark-pink: #b04b6f; +--base-pink: #d48fa8; +--light-pink: #f3d6df; + +1. Cyan (Monitoring / AI-detected anomaly) +--dark-cyan: #256b8a; +--base-cyan: #7fb2cc; +--light-cyan: #d6ecf5; + ### Medical References [Symbols ](https://hsps.pro/DentrixAscend/Help/Charting_symbols.htm) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index be743ff..364d120 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "types": [ + "vitest/globals", + "@testing-library/jest-dom" + ], "esModuleInterop": true, "jsx": "react-jsx", "module": "ESNext", @@ -9,4 +13,4 @@ "noEmit": true, "noImplicitAny": false } -} +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index 04817cc..7707360 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,3 @@ -import childProcess from "node:child_process"; -import fs from "node:fs"; -import { readFile } from "node:fs/promises"; -import path from "node:path"; import { type Options, defineConfig } from "tsup"; const common: Options = { @@ -17,60 +13,4 @@ const common: Options = { injectStyle: false, }; -const getPackageName = async () => { - try { - const packageJson = JSON.parse( - await readFile(path.join(__dirname, "package.json"), "utf-8"), - ); - return packageJson.name; - } catch (_error) { - return "package-name"; - } -}; - -const _addUseStatement = async ( - basePath: string, - type: "server" | "client", -) => { - const fullPath = path.join(__dirname, basePath); - const files = fs.readdirSync(fullPath); - - for (const file of files) { - if (file.endsWith(".js") || file.endsWith(".mjs")) { - const filePath = path.join(fullPath, file); - let content = await readFile(filePath, "utf-8"); - content = `"use ${type}";\n${content}`; - fs.writeFileSync(filePath, content, "utf-8"); - } - } -}; - -const linkSelf = async () => { - await new Promise((resolve) => { - childProcess.exec("pnpm link:self", (error, _stdout, _stderr) => { - if (error) { - // biome-ignore lint/suspicious/noConsole: - console.error(`exec error: ${error}`); - return; - } - - resolve(undefined); - }); - }); - - // biome-ignore lint/suspicious/noConsoleLog: - // biome-ignore lint/suspicious/noConsole: - console.log( - `Run 'pnpm link ${await getPackageName()} --global' inside another project to consume this package.`, - ); -}; - -export default defineConfig({ - async onSuccess() { - // If you want need to add a use statement to files, you can use the following code: - // await _addUseStatement('dist/react', 'client'); - - await linkSelf(); - }, - ...common, -}); +export default defineConfig(common); diff --git a/vitest.config.mts b/vitest.config.mts index 5fa8a72..d649a34 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "jsdom", - setupFiles: "./tests/setup.js", + setupFiles: "./tests/setup.ts", passWithNoTests: true, coverage: { include: ["{src,tests}/**/*"],