Repository: img-mapper/react-img-mapper Branch: master Commit: 03304dfcacfb Files: 119 Total size: 163.9 KB Directory structure: gitextract_07nloskl/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── 1.bug.yml │ │ └── 2.feature.yml │ ├── pull_request_template.md │ ├── stale.yml │ └── workflows/ │ └── validate-pr.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .npmrc ├── .nvmrc ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── apps/ │ └── examples/ │ ├── .storybook/ │ │ ├── main.ts │ │ ├── preview.tsx │ │ ├── react-code-addon/ │ │ │ └── register.tsx │ │ └── vue-code-addon/ │ │ └── register.tsx │ ├── package.json │ ├── public/ │ │ └── assets/ │ │ └── areas.json │ ├── src/ │ │ ├── code/ │ │ │ ├── areas.ts │ │ │ ├── colors.ts │ │ │ ├── dynamic.ts │ │ │ ├── map.ts │ │ │ └── simple.ts │ │ ├── components/ │ │ │ ├── DynamicMapper.tsx │ │ │ ├── Mapper.tsx │ │ │ ├── TopComponent.tsx │ │ │ └── ZoomInZoomOutAreaComp.tsx │ │ ├── constants/ │ │ │ └── index.ts │ │ ├── functions/ │ │ │ ├── mapper.ts │ │ │ └── mapperWithState.ts │ │ ├── hooks/ │ │ │ └── useAreas.ts │ │ ├── stories/ │ │ │ ├── Area.stories.tsx │ │ │ ├── Colors.stories.tsx │ │ │ ├── Dynamic.stories.tsx │ │ │ ├── Map.stories.tsx │ │ │ └── Simple.stories.tsx │ │ ├── styles/ │ │ │ └── stories.css │ │ ├── templates/ │ │ │ ├── clearButtonTemplate.ts │ │ │ ├── variablesTemplate.ts │ │ │ └── zoomTemplate.ts │ │ └── types/ │ │ ├── globals.d.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vercel.json ├── docs/ │ ├── .vitepress/ │ │ ├── config.mts │ │ └── theme/ │ │ ├── index.ts │ │ └── style.css │ ├── contribute/ │ │ └── guide.md │ ├── guide/ │ │ ├── examples.md │ │ └── getting-started.md │ ├── index.md │ ├── package.json │ ├── react/ │ │ ├── installation.md │ │ └── properties.md │ ├── tsconfig.json │ ├── vercel.json │ └── vue/ │ ├── installation.md │ └── properties.md ├── eslint.config.mjs ├── lint/ │ ├── general.eslint.mjs │ ├── import.eslint.mjs │ ├── javascript.eslint.mjs │ ├── prettier.eslint.mjs │ ├── react.eslint.mjs │ ├── typescript.eslint.mjs │ └── utils.eslint.mjs ├── lint-staged.config.mjs ├── package.json ├── packages/ │ ├── react-img-mapper/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── playground/ │ │ │ ├── index.html │ │ │ └── src/ │ │ │ ├── ReactPlayground.tsx │ │ │ ├── hooks/ │ │ │ │ └── useAreas.ts │ │ │ └── main.tsx │ │ ├── src/ │ │ │ ├── @types/ │ │ │ │ ├── area.d.ts │ │ │ │ ├── constants.d.ts │ │ │ │ ├── dimensions.d.ts │ │ │ │ ├── draw.d.ts │ │ │ │ ├── events.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── lib.d.ts │ │ │ │ └── styles.d.ts │ │ │ ├── ImageMapper.tsx │ │ │ ├── helpers/ │ │ │ │ ├── area.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── dimensions.ts │ │ │ │ ├── draw.ts │ │ │ │ ├── events.ts │ │ │ │ └── styles.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.mts │ │ └── vite.config.ts │ └── vue-img-mapper/ │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── playground/ │ │ ├── index.html │ │ └── src/ │ │ ├── App.vue │ │ └── index.ts │ ├── src/ │ │ ├── ImageMapper.vue │ │ ├── helpers/ │ │ │ └── area.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tsdown.config.mts │ └── vite.config.ts ├── pnpm-workspace.yaml ├── prettier.config.mjs ├── scripts/ │ └── lint.sh └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", "changelog": false, "commit": false, "fixed": [["react-img-mapper"]], "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .gitattributes ================================================ # Project files .gitattributes text eol=lf .gitignore text eol=lf .npmrc text eol=lf .nvmrc text eol=lf .prettierignore text eol=lf # Global text-based files *.{js,cjs,mjs,jsx,ts,cts,mts,tsx,json,yaml,yml,sh,md,txt} text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ * @NishargShah ================================================ FILE: .github/ISSUE_TEMPLATE/1.bug.yml ================================================ name: Bug report 🐛 description: Create a bug report assignees: - NishargShah labels: - new - bug body: - type: markdown attributes: value: Thanks for contributing by creating an issue! ❤️ - type: textarea attributes: label: Steps to reproduce description: | **⚠️ Issues that we can't reproduce can't be fixed.** Please provide the steps to reproduce the behavior: value: | Steps: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: true - type: textarea attributes: label: Current behavior description: Describe what happens instead of the expected behavior. validations: required: true - type: textarea attributes: label: Expected behavior description: Describe what should happen. validations: required: true - type: textarea attributes: label: Context description: What are you trying to accomplish? Providing context helps us come up with a solution that is more useful in the real world. - type: textarea attributes: label: Error stack description: Please provide the error stack of your error - type: input attributes: label: Live example link description: Please provide a link to a live example, you can use codesandbox/stackblitz for that validations: required: true - type: textarea attributes: label: Your environment description: Please provide your desktop & smartphone environment if applicable value: | Desktop - OS: [e.g. ubuntu] - Browser: [e.g. chrome, safari] - Version: [e.g. 22.04] Smartphone - Device: [e.g. samsung 24] - OS: [e.g. Android 15] - Browser: [e.g. chrome, safari] ================================================ FILE: .github/ISSUE_TEMPLATE/2.feature.yml ================================================ name: Feature request 🚀 description: Suggest an idea for this project assignees: - NishargShah labels: - new - enhancement body: - type: markdown attributes: value: Thanks for contributing by creating an issue! ❤️ - type: textarea attributes: label: Is your feature request related to a problem? description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: true - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. - type: textarea attributes: label: Context description: Add any other context or screenshots about the feature request here. ================================================ FILE: .github/pull_request_template.md ================================================ ## PR Checklist - [ ] Checked that there isn't already a PR that solves the problem the same way to avoid creating a duplicate. - [ ] Provided a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #000`). ### Description ### Linked Issues ### Additional context ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 14 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/validate-pr.yml ================================================ name: Validate Pull Request permissions: contents: read on: pull_request: branches: - master - canary jobs: validate_pr: name: Validating Pull Request runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install PNPM uses: pnpm/action-setup@v4 with: run_install: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build Project run: pnpm build - name: Checking Linting run: pnpm --silent script:lint --for=ci ================================================ FILE: .gitignore ================================================ # dependencies node_modules .pnp .pnp.js # testing coverage # production build dist cache # misc .DS_Store *.pem # debug *.log # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # editor folders .idea .vscode # vite .vite # storybook storybook-static ================================================ FILE: .husky/pre-commit ================================================ pnpm lint-staged ================================================ FILE: .husky/pre-push ================================================ pnpm --silent script:lint --for=check ================================================ FILE: .npmrc ================================================ script-shell=bash engine-strict=true ================================================ FILE: .nvmrc ================================================ 24 ================================================ FILE: .prettierignore ================================================ pnpm-lock.yaml ================================================ FILE: CHANGELOG.md ================================================ ## 2.1.0 (2025-10-00) ### 🚨 Breaking Change - Introduce `vue-img-mapper` package. - **react-img-mapper:** ESM is by default. ### 🚀 Features - **examples:** Added Vue Code support. ## 2.0.3 (2025-10-23) ### 🚀 Features - **docs:** Introduce official documentation of `img-mapper`. - **react-img-mapper:** Introduce playground for contributors. ### 🩹 Fixes - Contribution guidelines added. - **examples:** Examples descriptions changed. ## 2.0.2 (2025-10-19) ### 🚀 Features - **react-img-mapper:** Added ESM support. ### 🩹 Fixes - **react-img-mapper:** Required `ref` issue fixed. ## 2.0.1 (2025-10-18) ### 🚨 Breaking Change - Monorepo introduced. - Repository renamed from `react-img-mapper` to `img-mapper`. ### 🚀 Features - **react-docs:** Upgrade storybook to latest and improve the code. ### 🩹 Fixes - **react-img-mapper:** Fixed #96 issue. ### ❤️ Thank You - @sheepysheepy ## 2.0.0 (2025-01-26) ### 🚨 Breaking Change - **react-img-mapper:** Wrote a library from scratch. - **react-img-mapper:** `map.name` prop changed to `name`. - **react-img-mapper:** `map.areas` prop changed to `areas`. - **react-img-mapper:** `containerRef` prop removed, you can directly use `ref` instead. - **react-img-mapper:** `stayHighlighted` prop changed to `isMulti: false`. - **react-img-mapper:** `stayMultiHighlighted` prop changed to `isMulti: true`. - **react-img-mapper:** `toggleHighlighted` prop changed to `toggle: true`. - **react-img-mapper:** `rerenderProps` prop removed. - **react-img-mapper:** `clearHighlightedArea` method removed. - **react-img-mapper:** Typescript types are changed. - `MapAreas` changed to `MapArea`. - `CustomArea` changed to `Area`. ### 🚀 Features - **react-img-mapper:** React 19 upgrade added. - **react-img-mapper:** Converted non-controllable manner functionality to a controllable manner. - **react-img-mapper:** Typescript Reformatted. - **react-img-mapper:** New Utilities files added. - **react-img-mapper:** Removed `yarn` and added `pnpm`. ### 🩹 Fixes - **react-img-mapper:** Fixed #66 issue. - **react-img-mapper:** Fixed #76 issue. - **react-img-mapper:** Fixed #83 issue. ### ❤️ Thank You - Ethan Carlson @ethan-carlson - Melih Çoban @melihcoban - @sheepysheepy ## 1.5.0 (2023-02-14) ### 🚨 Breaking Change - **react-img-mapper:** Fully Compatible with Next.js. ### 🚀 Features - **react-img-mapper:** Added different classnames for highlighted areas. - The highlighted area will have `img-mapper-area-highlighted` classname in their area tag. - **react-img-mapper:** Upgrade to React V18 Peer Dep. ### 🩹 Fixes - **react-img-mapper:** Removed the previously highlighted area when you click on the new highlighted area when stayHighlighted is applied (https://github.com/img-mapper/react-img-mapper/issues/53). - **react-img-mapper:** Area JSON preFillColor will not remove when the toggleHighlighted property is applied. - **react-img-mapper:** Fixed infinity coords issue (https://github.com/img-mapper/react-img-mapper/issues/42). - **react-img-mapper:** Fixed canvas height and width issue (https://github.com/img-mapper/react-img-mapper/issues/43). ### ❤️ Thank You - Anders Weinstein @andersweinstein - Alba Mateos @albmat - GAURAV YADAV @DVGY ## 1.4.0 (2022-03-06) ### 🩹 Fixes - **react-img-mapper:** Resolved `onLoad` issue for Next.js. ## 1.3.0 ### ⚠️ Deprecated ## 1.2.0 (2021-07-12) ### 🚀 Features - **react-img-mapper:** Compatible with CommonJS. ## 1.1.0 (2021-03-29) ### 🚀 Features - **react-img-mapper:** Added Disabled Property in Area. - **react-img-mapper:** Added Disabled and Active Properties In JSON example. ## 1.0.0 (2021-03-21) ### 🚨 Breaking Change - **react-img-mapper:** Built in TypeScript. ## 0.5.0 (2021-02-13) ### 🚨 Breaking Change - Shifted to new organization `img-mapper`. ### 🚀 Features - **react-docs:** Added every property & method example with the code in documentation. - **react-img-mapper:** Removed Documentation from the package and shifted to another repo. ## 0.4.0 (2021-01-23) ### 🚀 Features - **react-docs:** Storybook documentation added. ### 🩹 Fixes - **react-img-mapper:** Internal bugs fixed. ## 0.3.0 (2021-01-22) ### 🚀 Features - **react-img-mapper:** Added highlighted map after clicking on the image. - **react-img-mapper:** Added a responsive image mapper. - **react-img-mapper:** Added Image Reference in Width, Height, and onLoad function to access image properties. - **react-img-mapper:** Added rerenderProps prop. ## 0.2.0 (2021-01-10) ### 🚀 Features - **react-img-mapper:** Added Natural Dimensions options ( For Network Image ). - **react-img-mapper:** Added Babel & ESLint in the example folder, for better formatting and creating compiled files. ## 0.1.0 (2021-01-10) ### 🚀 Features - **react-img-mapper:** Decreased size of bundled. - **react-img-mapper:** Compatible for NPM. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at nishargshah3101@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Please refer [Contributing](https://img-mapper.nishargshah.dev/contribute/guide). ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2025 Nisharg Shah Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Img Mapper

Documentation | Examples | Contributing

npm version npm downloads npm last updated

Libraries for Creating Interactive and Highlighted Zones on Images. - **`react-img-mapper`**: A React component that lets you define, highlight, and interact with custom zones on images. - **`vue-img-mapper`**: A Vue component offering the same interactive image mapping and highlighting capabilities. ## License This project is licensed under the [MIT License](https://opensource.org/licenses/mit-license.php). ================================================ FILE: SECURITY.md ================================================ # Security Policy If you discover a security vulnerability in this project, please report it responsibly: 1. **Do not** create a public issue. 2. Send an email to **nishargshah3101@gmail.com** with: - The nature of the vulnerability. - Steps to reproduce. - Version(s) affected. - Suggested fix or mitigation, if possible. 3. We aim to respond within **72 hours**. After confirming the issue, we’ll prepare a fix and release a patched version. Once the patch is published, we will disclose the vulnerability publicly via GitHub. ## Acknowledgments We appreciate and welcome reports from the community. If you wish, we can credit you by name (or anonymously) in our release notes after publishing a fix. ================================================ FILE: apps/examples/.storybook/main.ts ================================================ import path from 'node:path'; import type { StorybookConfig } from '@storybook/react-vite'; const config = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: ['./react-code-addon/register.tsx', './vue-code-addon/register.tsx'], framework: { name: '@storybook/react-vite', options: {}, }, staticDirs: ['../public'], features: { interactions: false, actions: false, }, viteFinal: (viteConfig) => { const { root } = viteConfig; if (!root) return viteConfig; return { ...viteConfig, resolve: { ...viteConfig.resolve, alias: { ...(Array.isArray(viteConfig.resolve?.alias) ? null : (viteConfig.resolve?.alias as Record)), '@': path.resolve(root, 'src'), }, }, }; }, } as StorybookConfig; export default config; ================================================ FILE: apps/examples/.storybook/preview.tsx ================================================ import { Fragment } from 'react'; import { Analytics } from '@vercel/analytics/react'; import '@/styles/stories.css'; import type { Preview } from '@storybook/react-vite'; const preview = { decorators: [ (Story) => ( ), ], parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, disableSaveFromUI: true, }, actions: { argTypesRegex: '^on[A-Z].*', }, options: { storySort: { order: [ 'Examples', ['Simple', 'Colors', 'Area', 'Responsive Map', 'Dynamic All Properties'], ], }, }, }, } as Preview; export default preview; ================================================ FILE: apps/examples/.storybook/react-code-addon/register.tsx ================================================ /* eslint-disable import-x/no-extraneous-dependencies */ // DON'T REMOVE REACT FROM HERE import React from 'react'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; import { AddonPanel } from 'storybook/internal/components'; import { addons, types, useParameter } from 'storybook/manager-api'; const ReactContent = () => { const reactCode = useParameter('reactCode', 'No Code Available'); return ( {reactCode} ); }; addons.register('my/react-code-addon', () => { addons.add('react-code-addon/panel', { title: 'React', type: types.PANEL, render: ({ active }) => ( ), }); }); ================================================ FILE: apps/examples/.storybook/vue-code-addon/register.tsx ================================================ /* eslint-disable import-x/no-extraneous-dependencies */ // DON'T REMOVE REACT FROM HERE import React from 'react'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; import { AddonPanel } from 'storybook/internal/components'; import { addons, types, useParameter } from 'storybook/manager-api'; const VueContent = () => { const vueCode = useParameter('vueCode', 'No Code Available'); return ( {vueCode} ); }; addons.register('my/vue-code-addon', () => { addons.add('vue-code-addon/panel', { title: 'Vue', type: types.PANEL, render: ({ active }) => ( ), }); }); ================================================ FILE: apps/examples/package.json ================================================ { "name": "examples", "version": "2.0.3", "private": true, "description": "Examples of react-img-mapper and vue-img-mapper", "bugs": { "url": "https://github.com/img-mapper/img-mapper/issues" }, "repository": { "type": "git", "url": "https://github.com/img-mapper/img-mapper.git", "directory": "apps/examples" }, "license": "MIT", "author": "Nisharg Shah ", "type": "module", "scripts": { "prebuild": "pnpm build:react", "build": "storybook build", "build:react": "pnpm --filter react-img-mapper build", "predev": "pnpm build:react", "dev": "storybook dev -p 3002", "typecheck": "tsc --noEmit" }, "dependencies": { "@vercel/analytics": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-img-mapper": "workspace:*", "react-syntax-highlighter": "^15.6.6" }, "devDependencies": { "@storybook/react-vite": "^9.1.13", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/react-syntax-highlighter": "^15.5.13", "storybook": "^9.1.13", "typescript": "catalog:" } } ================================================ FILE: apps/examples/public/assets/areas.json ================================================ [ { "id": "469f9800-c45a-483f-b13e-bd24f3fb79f4", "title": "Hardwood", "shape": "poly", "name": "1", "fillColor": "#eab54d4d", "strokeColor": "black", "coords": [ 520.0646766169153, 393.0348258706467, 85.23880597014923, 378.6069651741293, 637, 479, 13.099502487562177, 478.10945273631836, 11.606965174129343, 438.3084577114427 ], "polygon": [ [520.0646766169153, 393.0348258706467], [85.23880597014923, 378.6069651741293], [637, 479], [13.099502487562177, 478.10945273631836], [11.606965174129343, 438.3084577114427] ] }, { "id": "1db62daa-22a4-4b02-b5c0-fffdcf77c66c", "title": "Carpet", "shape": "poly", "name": "2", "fillColor": "#eab54d4d", "strokeColor": "black", "coords": [ 126.5323383084577, 345.273631840796, 465.3383084577114, 349.25373134328356, 520.0646766169153, 393.0348258706467, 85.23880597014923, 378.6069651741293 ], "polygon": [ [126.5323383084577, 345.273631840796], [465.3383084577114, 349.25373134328356], [520.0646766169153, 393.0348258706467], [85.23880597014923, 378.6069651741293] ] }, { "id": "667d73b1-4583-4080-ab6b-5759f25440bb", "title": "Materials", "shape": "poly", "name": "3", "fillColor": "#eab54d4d", "strokeColor": "black", "coords": [], "polygon": [] }, { "id": "a87203cb-3916-48ea-856f-2bacab8b7eda", "title": "Floor", "shape": "poly", "name": "4", "fillColor": "#eab54d4d", "strokeColor": "black", "coords": [ 130.0149253731343, 341.2935323383084, 462.8507462686566, 347.7611940298507, 637, 479, 13.099502487562177, 478.10945273631836, 11.606965174129343, 438.3084577114427 ], "polygon": [ [130.0149253731343, 341.2935323383084], [462.8507462686566, 347.7611940298507], [637, 479], [13.099502487562177, 478.10945273631836], [11.606965174129343, 438.3084577114427] ] }, { "id": "37ed1569-1e68-4816-9033-1a88c53b39df", "title": "Electrical Fixture", "shape": "poly", "name": "5", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 521.0597014925372, 335.820895522388, 528.0248756218905, 338.30845771144277, 527.0298507462686, 354.228855721393, 518.0746268656716, 349.25373134328356 ], "polygon": [ [521.0597014925372, 335.820895522388], [528.0248756218905, 338.30845771144277], [527.0298507462686, 354.228855721393], [518.0746268656716, 349.25373134328356] ] }, { "id": "ce471cbe-4103-45cc-899c-2be6497dc79a", "title": "Electrical Fixture", "shape": "poly", "name": "6", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 531.5074626865671, 342.2885572139303, 538.4726368159203, 342.78606965174123, 538.4726368159203, 357.7114427860696, 530.5124378109452, 355.72139303482584 ], "polygon": [ [531.5074626865671, 342.2885572139303], [538.4726368159203, 342.78606965174123], [538.4726368159203, 357.7114427860696], [530.5124378109452, 355.72139303482584] ] }, { "id": "5fde0edd-4e1c-4130-9ee5-4ec6dfd34f46", "title": "Electrical Fixture", "shape": "poly", "name": "7", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 589.2189054726367, 136.31840796019898, 605.6368159203979, 133.83084577114425, 604.1442786069651, 153.73134328358208, 590.7114427860696, 153.23383084577114 ], "polygon": [ [589.2189054726367, 136.31840796019898], [605.6368159203979, 133.83084577114425], [604.1442786069651, 153.73134328358208], [590.7114427860696, 153.23383084577114] ] }, { "id": "976082e0-0653-4e5d-8094-cc351e482e72", "title": "Electrical Fixture", "shape": "poly", "name": "8", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 606.6318407960198, 130.8457711442786, 619.5671641791045, 129.8507462686567, 621.0597014925372, 152.73631840796017, 606.1343283582089, 155.72139303482587 ], "polygon": [ [606.6318407960198, 130.8457711442786], [619.5671641791045, 129.8507462686567], [621.0597014925372, 152.73631840796017], [606.1343283582089, 155.72139303482587] ] }, { "id": "cc3c2799-ce62-4236-b4f6-6f4b50e7b666", "title": "GWB", "shape": "poly", "name": "9", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 521.5621890547263, 103.98009950248755, 508.6268656716418, 381.09452736318406, 638.9701492537313, 28.358208955223876, 637.9751243781094, 477.6119402985074 ], "polygon": [ [521.5621890547263, 103.98009950248755], [508.6268656716418, 381.09452736318406], [638.9701492537313, 28.358208955223876], [637.9751243781094, 477.6119402985074] ] }, { "id": "6c682813-8162-42eb-b3a7-c7296a009b5a", "title": "Brick", "shape": "poly", "name": "10", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 465.8358208955224, 137.81094527363183, 520.5621890547263, 103.98009950248755, 507.6268656716418, 381.09452736318406, 464.3432835820895, 350.7462686567164 ], "polygon": [ [465.8358208955224, 137.81094527363183], [520.5621890547263, 103.98009950248755], [507.6268656716418, 381.09452736318406], [464.3432835820895, 350.7462686567164] ] }, { "id": "1c9cabf2-4306-46cd-9423-63c7156cf4d4", "title": "Materials", "shape": "poly", "name": "11", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [], "polygon": [] }, { "id": "53c311f7-4e1c-4636-ac7e-b9cdec0d7ab7", "title": "Right Wall", "shape": "poly", "name": "12", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 465.8358208955224, 138.8059701492537, 638.9701492537313, 28.358208955223876, 637.9751243781094, 477.6119402985074, 463.8457711442785, 349.25373134328356 ], "polygon": [ [465.8358208955224, 138.8059701492537], [638.9701492537313, 28.358208955223876], [637.9751243781094, 477.6119402985074], [463.8457711442785, 349.25373134328356] ] }, { "id": "21a3befd-c97b-476d-8e0c-7c98399988bf", "title": "Window", "shape": "poly", "name": "13", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 211.10945273631836, 161.6915422885572, 387.7263681592039, 164.67661691542287, 383.7462686567164, 292.5373134328358, 207.62686567164178, 288.5572139303482 ], "polygon": [ [211.10945273631836, 161.6915422885572], [387.7263681592039, 164.67661691542287], [383.7462686567164, 292.5373134328358], [207.62686567164178, 288.5572139303482] ] }, { "id": "2f36ad1d-b934-4fb0-9486-7f429ef46a1b", "title": "Front Wall", "shape": "poly", "name": "14", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 131.50746268656715, 131.34328358208953, 465.3383084577114, 138.30845771144277, 462.35323383084574, 347.7611940298507, 129.51741293532336, 341.79104477611935 ], "polygon": [ [131.50746268656715, 131.34328358208953], [465.3383084577114, 138.30845771144277], [462.35323383084574, 347.7611940298507], [129.51741293532336, 341.79104477611935] ] }, { "id": "f3653fb6-c1c5-4fe7-aec1-699d9da7bba1", "title": "Microwave", "shape": "poly", "name": "15", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 120.06467661691539, 193.5323383084577, 145.93532338308455, 197.51243781094524, 146.4328358208955, 234.82587064676613, 118.07462686567163, 233.33333333333331 ], "polygon": [ [120.06467661691539, 193.5323383084577], [145.93532338308455, 197.51243781094524], [146.4328358208955, 234.82587064676613], [118.07462686567163, 233.33333333333331] ] }, { "id": "eca521ca-11c6-4312-830b-3492829649df", "title": "Stove", "shape": "poly", "name": "16", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 85.73631840796017, 254.22885572139302, 85.73631840796017, 279.10447761194024, 139.46766169154228, 282.08955223880594, 162.85074626865668, 276.1194029850746, 118.07462686567163, 274.6268656716418, 117.57711442786066, 264.67661691542287, 115.08955223880594, 249.7512437810945 ], "polygon": [ [85.73631840796017, 254.22885572139302], [85.73631840796017, 279.10447761194024], [139.46766169154228, 282.08955223880594], [162.85074626865668, 276.1194029850746], [118.07462686567163, 274.6268656716418], [117.57711442786066, 264.67661691542287], [115.08955223880594, 249.7512437810945] ] }, { "id": "e8da6027-7563-4a50-9b7b-9ffc1bb1b613", "title": "Oven", "shape": "poly", "name": "17", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 145.4378109452736, 288.0597014925373, 164.34328358208953, 282.08955223880594, 166.33333333333331, 353.731343283582, 142.45273631840794, 371.6417910447761 ], "polygon": [ [145.4378109452736, 288.0597014925373], [164.34328358208953, 282.08955223880594], [166.33333333333331, 353.731343283582], [142.45273631840794, 371.6417910447761] ] }, { "id": "5248f935-10c8-4b16-8cff-21b66d2cb56f", "title": "Countertop", "shape": "poly", "name": "18", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 70.31343283582089, 287.56218905472633, 82.25373134328356, 281.09452736318406, 140.46268656716416, 283.0845771144278, 77.77611940298505, 303.4825870646766 ], "polygon": [ [70.31343283582089, 287.56218905472633], [82.25373134328356, 281.09452736318406], [140.46268656716416, 283.0845771144278], [77.77611940298505, 303.4825870646766] ] }, { "id": "5b40c828-ecb3-4633-b181-78e2832823b1", "title": "Double Cabinet", "shape": "poly", "name": "19", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 108.62189054726366, 298.5074626865671, 139.46766169154228, 289.5522388059701, 138.47263681592037, 364.1791044776119, 106.13432835820893, 390.54726368159197 ], "polygon": [ [108.62189054726366, 298.5074626865671], [139.46766169154228, 289.5522388059701], [138.47263681592037, 364.1791044776119], [106.13432835820893, 390.54726368159197] ] }, { "id": "7810e113-49d2-4284-9e49-318af8378663", "title": "Dishwasher", "shape": "poly", "name": "20", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 81.25870646766168, 308.45771144278604, 108.12437810945272, 300.4975124378109, 106.13432835820893, 393.53233830845767, 80.26368159203977, 410.4477611940298 ], "polygon": [ [81.25870646766168, 308.45771144278604], [108.12437810945272, 300.4975124378109], [106.13432835820893, 393.53233830845767], [80.26368159203977, 410.4477611940298] ] }, { "id": "5998531a-25b3-4288-adbe-53c4470a369b", "title": "Refrigerator", "shape": "poly", "name": "21", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 18.572139303482572, 169.65174129353233, 82.25373134328356, 182.5870646766169, 80.76119402985074, 424.8756218905472, 14.09452736318407, 475.6218905472636 ], "polygon": [ [18.572139303482572, 169.65174129353233], [82.25373134328356, 182.5870646766169], [80.76119402985074, 424.8756218905472], [14.09452736318407, 475.6218905472636] ] }, { "id": "9db9f57d-c15e-4d3a-abb7-faa7e69657c8", "title": "Single Cabinet", "shape": "poly", "name": "22", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 140.46268656716416, 142.28855721393032, 149.91542288557213, 148.75621890547262, 147.4278606965174, 234.3283582089552, 138.9701492537313, 232.3383084577114 ], "polygon": [ [140.46268656716416, 142.28855721393032], [149.91542288557213, 148.75621890547262], [147.4278606965174, 234.3283582089552], [138.9701492537313, 232.3383084577114] ] }, { "id": "d2c06088-49ce-404b-ab78-d865a336248d", "title": "Double Cabinet", "shape": "poly", "name": "23", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 111.10945273631839, 128.35820895522386, 139.46766169154228, 142.7860696517413, 139.96517412935322, 196.01990049751242, 112.10447761194027, 191.04477611940297 ], "polygon": [ [111.10945273631839, 128.35820895522386], [139.46766169154228, 142.7860696517413], [139.96517412935322, 196.01990049751242], [112.10447761194027, 191.04477611940297] ] }, { "id": "07feade7-e370-4384-bb96-c21f9eedb238", "title": "Double Cabinet", "shape": "poly", "name": "24", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 72.80099502487562, 108.45771144278606, 111.60696517412933, 127.36318407960198, 112.60199004975124, 233.83084577114425, 74.79104477611938, 234.82587064676613 ], "polygon": [ [72.80099502487562, 108.45771144278606], [111.60696517412933, 127.36318407960198], [112.60199004975124, 233.83084577114425], [74.79104477611938, 234.82587064676613] ] }, { "id": "db82b663-6c46-4a21-9ba6-fa6aa5bf84dc", "title": "Double Cabinet", "shape": "poly", "name": "25", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 19.567164179104466, 56.21890547263681, 71.80597014925371, 87.56218905472636, 71.80597014925371, 173.63184079601987, 18.572139303482572, 159.20398009950247 ], "polygon": [ [19.567164179104466, 56.21890547263681], [71.80597014925371, 87.56218905472636], [71.80597014925371, 173.63184079601987], [18.572139303482572, 159.20398009950247] ] }, { "id": "9258a68c-dc5d-4b08-bee1-720d8e8e3509", "title": "Left Wall", "shape": "poly", "name": "26", "fillColor": "#00ff194c", "strokeColor": "black", "coords": [ 20.064676616915406, 57.71144278606965, 131.50746268656715, 131.34328358208953, 130.0149253731343, 341.79104477611935, 12.104477611940283, 436.8159203980099 ], "polygon": [ [20.064676616915406, 57.71144278606965], [131.50746268656715, 131.34328358208953], [130.0149253731343, 341.79104477611935], [12.104477611940283, 436.8159203980099] ] }, { "id": "e30e9e21-0a03-4514-9473-887f23991361", "title": "Vent", "shape": "poly", "name": "27", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 249.91542288557213, 0, 299.66666666666663, 0.49751243781094523, 298.67164179104475, 13.930348258706466, 250.910447761194, 13.930348258706466 ], "polygon": [ [249.91542288557213, 0], [299.66666666666663, 0.49751243781094523], [298.67164179104475, 13.930348258706466], [250.910447761194, 13.930348258706466] ] }, { "id": "f5a8d660-61df-4783-a631-8ea6758ee50d", "title": "Vent", "shape": "poly", "name": "28", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 285.2388059701492, 117.41293532338307, 309.1194029850746, 116.91542288557213, 309.1194029850746, 128.8557213930348, 286.731343283582, 128.35820895522386 ], "polygon": [ [285.2388059701492, 117.41293532338307], [309.1194029850746, 116.91542288557213], [309.1194029850746, 128.8557213930348], [286.731343283582, 128.35820895522386] ] }, { "id": "6721b73c-a4f7-486c-8d72-5d7e817db59a", "title": "Light", "shape": "poly", "name": "29", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 266.83084577114425, 93.5323383084577, 277.2786069651741, 93.03482587064676, 277.2786069651741, 99.50248756218905, 267.3283582089552, 99.00497512437809 ], "polygon": [ [266.83084577114425, 93.5323383084577], [277.2786069651741, 93.03482587064676], [277.2786069651741, 99.50248756218905], [267.3283582089552, 99.00497512437809] ] }, { "id": "6fe8c503-66f8-47be-bad8-5a39d170e538", "title": "Recessed Light", "shape": "poly", "name": "30", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 206.1343283582089, 103.48258706467661, 227.0298507462686, 105.47263681592038, 222.5522388059701, 113.93034825870646, 207.12935323383084, 112.93532338308457 ], "polygon": [ [206.1343283582089, 103.48258706467661], [227.0298507462686, 105.47263681592038], [222.5522388059701, 113.93034825870646], [207.12935323383084, 112.93532338308457] ] }, { "id": "b5ef36ad-484b-4605-a2ba-72b9f1e7114f", "title": "Recessed Light", "shape": "poly", "name": "31", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 164.84079601990047, 55.721393034825866, 187.72636815920396, 54.72636815920397, 185.73631840796017, 66.16915422885572, 167.82587064676613, 66.66666666666666 ], "polygon": [ [164.84079601990047, 55.721393034825866], [187.72636815920396, 54.72636815920397], [185.73631840796017, 66.16915422885572], [167.82587064676613, 66.66666666666666] ] }, { "id": "75449960-7fde-4907-a463-7bb5b146d70c", "title": "Ceiling", "shape": "poly", "name": "32", "fillColor": "#ff000026", "strokeColor": "black", "coords": [ 19.567164179104466, 52.73631840796019, 19.567164179104466, 1.990049751243781, 637.9751243781094, 1.4925373134328357, 638.9701492537313, 28.358208955223876, 464.8407960199004, 138.8059701492537, 131.50746268656715, 130.34825870646765 ], "polygon": [ [19.567164179104466, 52.73631840796019], [19.567164179104466, 1.990049751243781], [637.9751243781094, 1.4925373134328357], [638.9701492537313, 28.358208955223876], [464.8407960199004, 138.8059701492537], [131.50746268656715, 130.34825870646765] ] } ] ================================================ FILE: apps/examples/src/code/areas.ts ================================================ import mapper from '@/functions/mapper'; import mapperWithState from '@/functions/mapperWithState'; export const showHighlightedAreaCode = mapper(`( )`); export const inArrayShowHighlightedAreaCode = mapper(`( )`); export const disabledAreaCode = mapper(`( )`); export const inArrayDisabledAreaCode = inArrayShowHighlightedAreaCode; export const staySelectedHighlightedAreaCode = mapperWithState(`( setAreas(newAreas)} isMulti={false} /> )`); export const stayMultipleSelectedHighlightedAreaCode = mapperWithState(`( setAreas(newAreas)} isMulti /> )`); export const toggleStayHighlightedAreaCode = mapperWithState(`( setAreas(newAreas)} isMulti={props.isMulti} // dynamic isMulti toggle={props.toggle} // dynamic toggle /> )`); export { default as clearSelectedHighlightedAreaCode } from '@/templates/clearButtonTemplate'; export { default as zoomInZoomOutAreaCode } from '@/templates/zoomTemplate'; ================================================ FILE: apps/examples/src/code/colors.ts ================================================ import mapper from '@/functions/mapper'; export const fillColorCode = mapper(`( )`); export const inArrayFillColorCode = mapper(`( )`); export const dynamicFillColorCode = mapper(`( )`); export const dynamicMixArrayFillColorCode = mapper(`( )`); export const strokeColorCode = mapper(`( )`); export const inArrayStrokeColorCode = mapper(`( )`); export const dynamicStrokeColorCode = mapper(`( )`); export const dynamicMixArrayStrokeColorCode = mapper(`( )`); ================================================ FILE: apps/examples/src/code/dynamic.ts ================================================ import mapper from '@/functions/mapper'; const dynamicAllPropertiesCode = mapper(`( )`); export default dynamicAllPropertiesCode; ================================================ FILE: apps/examples/src/code/map.ts ================================================ import mapper from '@/functions/mapper'; export const nonResponsiveDimensionsCode = mapper(`( )`); export const responsiveDimensionsCode = mapper(`( )`); export const allDimensionsCode = mapper(`( )`); ================================================ FILE: apps/examples/src/code/simple.ts ================================================ import mapper from '@/functions/mapper'; const simpleCode = mapper(`( )`); export default simpleCode; ================================================ FILE: apps/examples/src/components/DynamicMapper.tsx ================================================ import { Fragment, useEffect, useState } from 'react'; import ImageMapper from 'react-img-mapper'; import TopComponent from '@/components/TopComponent'; import CONSTANTS from '@/constants'; import { useAreas } from '@/hooks/useAreas'; import type { ImageMapperProps } from 'react-img-mapper'; import type { Component } from '@/types'; const { url, name } = CONSTANTS; type DynamicMapperProps = Omit; const DynamicMapper: Component = (props) => { const { areas: initialAreas } = useAreas(); const [areas, setAreas] = useState(initialAreas); useEffect(() => { if (areas.length === 0) { setAreas( initialAreas.map((cur) => { const temp = { ...cur }; if (['Front Wall', 'Window'].includes(cur.title)) { delete temp.fillColor; delete temp.strokeColor; return temp; } return temp; }), ); } }, [areas.length, initialAreas]); if (areas.length === 0) return null; return ( {TopComponent( 'Dynamic All Properties Example',

In this example, all the functionalities developed so far have been merged into a single demo.

Feel free to explore and have fun experimenting!

, )} setAreas(newAreas)} src={url} />
); }; export default DynamicMapper; ================================================ FILE: apps/examples/src/components/Mapper.tsx ================================================ import { Fragment, useCallback, useEffect, useState } from 'react'; import ImageMapper from 'react-img-mapper'; import CONSTANTS from '@/constants'; import { useAreas } from '@/hooks/useAreas'; import type { ReactNode } from 'react'; import type { ImageMapperProps } from 'react-img-mapper'; import type { Component } from '@/types'; interface TopComponentProps { resetAreas: () => void; } interface BottomComponentProps { resetAreas: () => void; } type MapperProps = Omit & { customJSON?: 0 | 1 | 2; customType?: 'fill' | 'stroke' | 'active' | 'disabled'; isOnChangeNeeded?: boolean; TopComponent?: (props: TopComponentProps) => ReactNode; BottomComponent?: (props: BottomComponentProps) => ReactNode; }; const { url, name } = CONSTANTS; const Mapper: Component = (props) => { const { customJSON, customType, isOnChangeNeeded, TopComponent, BottomComponent, ...restProps } = props; const { areas: initialAreas } = useAreas(); const [areas, setAreas] = useState(initialAreas); const getJSON = useCallback(() => { if (customJSON === 0) { return initialAreas.map((item) => { const temp = { ...item } as typeof item; if (customType === 'fill') { delete temp.fillColor; } if (customType === 'stroke') { delete temp.fillColor; delete temp.strokeColor; } return temp; }); } if (customJSON === 1) { return initialAreas.map((item) => { const temp = { ...item } as typeof item; if (['Front Wall', 'Window'].includes(item.title)) { if (customType === 'fill') { delete temp.fillColor; } if (customType === 'stroke') { delete temp.strokeColor; } return temp; } return temp; }); } if (customJSON === 2) { return initialAreas.map((item) => { const temp = { ...item } as typeof item; if (['Refrigerator', 'Window'].includes(item.title)) { if (customType === 'active') { temp.active = false; } if (customType === 'disabled') { temp.disabled = true; } return temp; } return temp; }); } return initialAreas; }, [initialAreas, customJSON, customType]); const resetAreas = () => setAreas(getJSON()); useEffect(() => { if (areas.length === 0) setAreas(getJSON()); }, [areas.length, getJSON]); if (areas.length === 0) return null; return ( {TopComponent ? : null} (isOnChangeNeeded ? setAreas(newAreas) : null)} src={url} /> {BottomComponent ? : null} ); }; export default Mapper; ================================================ FILE: apps/examples/src/components/TopComponent.tsx ================================================ import type { JSX, ReactNode } from 'react'; type TopComponentElement = (title: string, content: ReactNode) => JSX.Element; const TopComponent: TopComponentElement = (title, content) => (

{title}

{content}
); export default TopComponent; ================================================ FILE: apps/examples/src/components/ZoomInZoomOutAreaComp.tsx ================================================ import { useState } from 'react'; import Mapper from '@/components/Mapper'; import TopComponent from '@/components/TopComponent'; import type { ImageMapperProps } from 'react-img-mapper'; import type { Component } from '@/types'; type ZoomInZoomOutAreaCompProps = Pick; const ZoomInZoomOutAreaComp: Component = (props) => { const minWidth = 400; const { parentWidth = 100 } = props; const [zoom, setZoom] = useState(640); const handleZoom = (type: 'in' | 'out') => { setZoom((prev) => { if (prev <= minWidth && type === 'out') return prev; return type === 'in' ? prev + parentWidth : prev - parentWidth; }); }; return ( TopComponent( 'Zoom In & Zoom Out Area Example',

In this example, zoom is controlled via the parentWidth, which you can adjust using the Storybook Controls tab. Click the buttons below to see the live zoom effect in the image mapper:

, ) } /> ); }; export default ZoomInZoomOutAreaComp; ================================================ FILE: apps/examples/src/constants/index.ts ================================================ const CONSTANTS = { url: 'https://img-mapper-examples.nishargshah.dev/assets/example.jpg', name: 'my-map', areasUrl: 'https://img-mapper-examples.nishargshah.dev/assets/areas.json', }; export default CONSTANTS; ================================================ FILE: apps/examples/src/functions/mapper.ts ================================================ import variablesTemplate from '@/templates/variablesTemplate'; type Mapper = (code: string) => string; const mapper: Mapper = (code) => `import React from 'react'; import ImageMapper from 'react-img-mapper'; const Mapper = props => { ${variablesTemplate} return ${code} } export default Mapper;`; export default mapper; ================================================ FILE: apps/examples/src/functions/mapperWithState.ts ================================================ import CONSTANTS from '@/constants'; const { url, name, areasUrl } = CONSTANTS; type MapperWithState = (code: string) => string; const mapperWithState: MapperWithState = (code) => `import React from 'react'; import ImageMapper from 'react-img-mapper'; const Mapper = props => { const url = '${url}'; const name = '${name}'; // Get JSON from below URL as an example and put it into the useState hook // URL: ${areasUrl} const [areas, setAreas] = useState([]); return ${code} } export default Mapper;`; export default mapperWithState; ================================================ FILE: apps/examples/src/hooks/useAreas.ts ================================================ import { useEffect, useState } from 'react'; import CONSTANTS from '@/constants'; import type { MapArea } from 'react-img-mapper'; interface AreasHookOutput { areas: MapArea[]; } type AreasHook = () => AreasHookOutput; const { areasUrl } = CONSTANTS; export const useAreas: AreasHook = () => { const [areas, setAreas] = useState([]); useEffect(() => { (async () => { const res = await fetch(areasUrl); const json = await res.json(); setAreas(json); })(); }, []); return { areas }; }; ================================================ FILE: apps/examples/src/stories/Area.stories.tsx ================================================ import { clearSelectedHighlightedAreaCode, disabledAreaCode, inArrayDisabledAreaCode, inArrayShowHighlightedAreaCode, showHighlightedAreaCode, stayMultipleSelectedHighlightedAreaCode, staySelectedHighlightedAreaCode, toggleStayHighlightedAreaCode, zoomInZoomOutAreaCode, } from '@/code/areas'; import Mapper from '@/components/Mapper'; import TopComponent from '@/components/TopComponent'; import ZoomInZoomOutAreaComp from '@/components/ZoomInZoomOutAreaComp'; import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { title: 'Examples/Area', component: Mapper, tags: ['autodocs'], } as Meta; type Story = StoryObj; export const ShowHighlightedArea: Story = { render: (args) => { const { active } = args; return ( TopComponent( 'Show Highlighted Area Example',

In this example, you can use the Storybook Controls tab to dynamically choose whether to show or hide the highlighted areas using the active toggle button, based on your preference.

, ) } /> ); }, parameters: { reactCode: showHighlightedAreaCode, }, args: { active: true, }, argTypes: { active: { control: 'boolean', }, }, }; export const InArrayShowHighlightedArea: Story = { render: () => ( TopComponent( 'Show Highlighted Area from Area JSON Example',

This example demonstrates how to selectively show or hide active areas of an image. Here, the{' '} window and refrigerator areas are excluded from visibility.

Note: By default, the active property is set to true for the remaining areas.

, ) } /> ), parameters: { reactCode: inArrayShowHighlightedAreaCode, }, }; export const DisabledArea: Story = { render: (args) => { const { disabled } = args; return ( TopComponent( 'Disabled Area Example',

In this example, you can use the Storybook Controls tab to dynamically enable or disable event listeners and highlighted areas using the disabled toggle button, according to your preference.

, ) } /> ); }, parameters: { reactCode: disabledAreaCode, }, args: { disabled: true, }, argTypes: { disabled: { control: 'boolean', }, }, }; export const InArrayDisabledArea: Story = { render: () => ( TopComponent( 'Disabled Area from Area JSON Example',

This example demonstrates how to selectively{' '} enable or disable specific areas of an image. Here, the window and refrigerator areas are excluded from interaction.

Note: By default, the disabled property is set to false for the remaining areas.

, ) } /> ), parameters: { reactCode: inArrayDisabledAreaCode, }, }; export const StaySelectedHighlightedArea: Story = { render: () => ( TopComponent( 'Stay Selected Highlighted Area Example',

In this example, you can freeze specific{' '} areas to keep them highlighted by clicking, while still being able to highlight the remaining areas on hover.

, ) } /> ), parameters: { reactCode: staySelectedHighlightedAreaCode, }, }; export const StayMultipleSelectedHighlightedArea: Story = { render: () => ( TopComponent( 'Stay Multiple Selected Highlighted Area Example',

This example is similar to the{' '} Stay Selected Highlighted Area section, with the added feature of allowing you to freeze multiple highlighted areas simultaneously.

, ) } /> ), parameters: { reactCode: stayMultipleSelectedHighlightedAreaCode, }, }; export const ClearSelectedHighlightedArea: Story = { render: () => ( TopComponent( 'Clear Selected Highlighted Area Example',

You can clear the single or multiple selected highlighted areas by resetting the state to its initial value. Click the button below to see the changes live in the image mapper:

, ) } /> ), parameters: { reactCode: clearSelectedHighlightedAreaCode, }, }; export const ToggleStayHighlightedArea: Story = { render: (args) => { const { isMulti, toggle } = args; return ( TopComponent( 'Toggle Stay Highlighted Area Example',

This example introduces the toggle property, which allows you to toggle previously frozen highlighted areas on and off.

, ) } /> ); }, parameters: { reactCode: toggleStayHighlightedAreaCode, }, args: { isMulti: true, toggle: true, }, argTypes: { isMulti: { control: 'boolean', }, toggle: { control: 'boolean', }, }, }; export const ZoomInZoomOutArea: Story = { render: ({ parentWidth }) => , parameters: { reactCode: zoomInZoomOutAreaCode, }, args: { parentWidth: 100, }, argTypes: { parentWidth: { control: 'number', }, }, }; export default meta; ================================================ FILE: apps/examples/src/stories/Colors.stories.tsx ================================================ import { dynamicFillColorCode, dynamicMixArrayFillColorCode, dynamicMixArrayStrokeColorCode, dynamicStrokeColorCode, fillColorCode, inArrayFillColorCode, inArrayStrokeColorCode, strokeColorCode, } from '@/code/colors'; import Mapper from '@/components/Mapper'; import TopComponent from '@/components/TopComponent'; import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { title: 'Examples/Colors', component: Mapper, tags: ['autodocs'], } as Meta; type Story = StoryObj; export const FillColor: Story = { render: () => ( TopComponent( 'Fill Color Example',

In this example, the fillColor property is not defined in the areas JSON. As a result, the mapper uses its default fillColor behavior.

, ) } /> ), parameters: { reactCode: fillColorCode, }, }; export const InArrayFillColor: Story = { render: () => ( TopComponent( 'Fill Color from Area JSON Example',

In this example, the fillColor property is defined in the areas JSON. Therefore, the mapper applies the fillColor values from the JSON, resulting in different fillColor for each area.

, ) } /> ), parameters: { reactCode: inArrayFillColorCode, }, }; export const DynamicFillColor: Story = { render: (args) => { const { fillColor } = args; return ( TopComponent( 'Dynamic Fill Color Example',

In this example, you can use the Storybook Controls tab to dynamically modify the fillColor property according to your preference.

Note: For better visual results, try reducing the opacity of the fillColor.

, ) } /> ); }, parameters: { reactCode: dynamicFillColorCode, }, args: { fillColor: 'rgba(255, 255, 255, 0.5)', }, argTypes: { fillColor: { control: 'color', }, }, }; export const DynamicMixArrayFillColor: Story = { render: (args) => { const { fillColor } = args; return ( TopComponent( 'Dynamic Mix Array Fill Color Example',

In this example, we demonstrate how to exclude a specific area of an image from the whole mapping. Here, the{' '} wall area is excluded, any changes made to the fillColor property from the Controls tab will only apply to the{' '} wall area.

Note: The fillColor property for the remaining areas is already defined in the JSON data.

, ) } /> ); }, parameters: { reactCode: dynamicMixArrayFillColorCode, }, args: { fillColor: 'rgba(255, 255, 255, 0.5)', }, argTypes: { fillColor: { control: 'color', }, }, }; export const StrokeColor: Story = { render: () => ( TopComponent( 'Stroke Color Example',

In this example, the strokeColor property is not defined in the areas JSON. Therefore, the mapper applies its default strokeColor behavior.

, ) } /> ), parameters: { reactCode: strokeColorCode, }, }; export const InArrayStrokeColor: Story = { render: () => ( TopComponent( 'Stroke Color from Area JSON Example',

In this example, the strokeColor property is defined in the areas JSON. Hence, the mapper applies the strokeColor values directly from the JSON.

, ) } /> ), parameters: { reactCode: inArrayStrokeColorCode, }, }; export const DynamicStrokeColor: Story = { render: (args) => { const { strokeColor, lineWidth } = args; return ( TopComponent( 'Dynamic Stroke Color Example',

In this example, you can use the Storybook Controls tab to dynamically adjust the strokeColor and{' '} lineWidth properties according to your preference.

, ) } /> ); }, parameters: { reactCode: dynamicStrokeColorCode, }, args: { strokeColor: 'rgba(0, 0, 0, 0.5)', lineWidth: 1, }, argTypes: { strokeColor: { control: 'color', }, lineWidth: { control: 'number', }, }, }; export const DynamicMixArrayStrokeColor: Story = { render: (args) => { const { strokeColor, lineWidth } = args; return ( TopComponent( 'Dynamic Mix Array Stroke Color Example',

This example demonstrates how to exclude a specific area of an image from the whole mapping. Here, the{' '} wall area is excluded, so any changes to the strokeColor property from the Controls tab will only apply to the{' '} wall area. Changes to the{' '} lineWidth property, however, will be applied to all areas.

Note: The strokeColor for the remaining areas is already defined in the JSON data.

, ) } /> ); }, parameters: { reactCode: dynamicMixArrayStrokeColorCode, }, args: { strokeColor: 'rgba(0, 0, 0, 0.5)', lineWidth: 1, }, argTypes: { strokeColor: { control: 'color', }, lineWidth: { control: 'number', }, }, }; export default meta; ================================================ FILE: apps/examples/src/stories/Dynamic.stories.tsx ================================================ import dynamicAllPropertiesCode from '@/code/dynamic'; import DynamicMapper from '@/components/DynamicMapper'; import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { title: 'Examples/Dynamic All Properties', component: DynamicMapper, tags: ['autodocs'], } as Meta; type Story = StoryObj; export const DynamicAllProperties: Story = { render: (args) => , parameters: { reactCode: dynamicAllPropertiesCode, }, args: { isMulti: true, toggle: false, active: true, disabled: false, fillColor: 'rgba(255, 255, 255, 0.5)', strokeColor: 'rgba(0, 0, 0, 0.5)', lineWidth: 1, imgWidth: 0, width: 0, height: 0, natural: false, responsive: false, parentWidth: 0, }, argTypes: { isMulti: { control: 'boolean', }, toggle: { control: 'boolean', }, active: { control: 'boolean', }, disabled: { control: 'boolean', }, fillColor: { control: 'color', }, strokeColor: { control: 'color', }, lineWidth: { control: 'number', }, imgWidth: { control: 'number', }, width: { control: 'number', }, height: { control: 'number', }, natural: { control: 'boolean', }, responsive: { control: 'boolean', }, parentWidth: { control: 'number', }, }, }; export default meta; ================================================ FILE: apps/examples/src/stories/Map.stories.tsx ================================================ import { allDimensionsCode, nonResponsiveDimensionsCode, responsiveDimensionsCode, } from '@/code/map'; import Mapper from '@/components/Mapper'; import TopComponent from '@/components/TopComponent'; import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { title: 'Examples/Responsive Map', component: Mapper, tags: ['autodocs'], } as Meta; type Story = StoryObj; export const NonResponsiveDimensions: Story = { render: (args) => { const { width, height, imgWidth, natural } = args; return ( TopComponent( 'Non Responsive Dimensions Example',

In this example, the width,{' '} height,imgWidth, and{' '} natural properties are available in the Storybook{' '} Controls tab. You can adjust them to see the{' '} live results in the image mapper.

Experimenting with different values in these fields highlights that making the image mapper fully responsive can be challenging.

Note: Full descriptions and explanations of all properties are available in the GitHub repository.

, ) } /> ); }, parameters: { reactCode: nonResponsiveDimensionsCode, }, args: { width: 640, height: 480, imgWidth: 0, natural: false, }, argTypes: { width: { control: 'number', }, height: { control: 'number', }, imgWidth: { control: 'number', }, natural: { control: 'boolean', }, }, }; export const ResponsiveDimensions: Story = { render: (args) => { const { parentWidth } = args; return ( TopComponent( 'Responsive Dimensions Example',

In this example, the responsive and{' '} parentWidth properties are available in the Storybook Controls tab. You can adjust them to see the live results in the image mapper.

By experimenting with different values for parentWidth, you'll notice that the mapper becomes responsive. Try copying the code and see the results, kudos!

Note: Full descriptions and explanations of all properties are available in the GitHub repository.

, ) } /> ); }, parameters: { reactCode: responsiveDimensionsCode, }, args: { parentWidth: 640, }, argTypes: { parentWidth: { control: 'number', }, }, }; export const AllDimensions: Story = { render: (args) => { const { width, height, imgWidth, natural, responsive, parentWidth } = args; return ( TopComponent( 'All Dimensions Example',

In this example, the width,{' '} height,imgWidth,{' '} natural,responsive, and{' '} parentWidth fields are available in the Storybook{' '} Controls tab. You can modify them to see the{' '} live results in the image mapper.

This example combines all responsive and non-responsive properties, have fun experimenting!

Note: Full descriptions and explanations of all properties are available in the GitHub repository.

, ) } /> ); }, parameters: { reactCode: allDimensionsCode, }, args: { width: 640, height: 480, imgWidth: 0, natural: false, responsive: false, parentWidth: 640, }, argTypes: { width: { control: 'number', }, height: { control: 'number', }, imgWidth: { control: 'number', }, natural: { control: 'boolean', }, responsive: { control: 'boolean', }, parentWidth: { control: 'number', }, }, }; export default meta; ================================================ FILE: apps/examples/src/stories/Simple.stories.tsx ================================================ import simpleCode from '@/code/simple'; import Mapper from '@/components/Mapper'; import TopComponent from '@/components/TopComponent'; import type { Meta, StoryObj } from '@storybook/react-vite'; const meta = { title: 'Examples/Simple', component: Mapper, tags: ['autodocs'], } as Meta; type Story = StoryObj; export const Simple: Story = { render: () => ( TopComponent( 'Simple Example',

Basic example showcasing the default setup and essential required properties.

, ) } /> ), parameters: { reactCode: simpleCode, }, }; export default meta; ================================================ FILE: apps/examples/src/styles/stories.css ================================================ :root { --tag: #1b1f230d; --block: #efefef; --block-border: #cecece; --tag-block: #f6f8fa; } body { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; } p { margin: 0; } .tag { padding: 0.2em 0.4em; margin: 0; font-size: 85%; background-color: var(--tag); border-radius: 6px; } .block { background: var(--block); border-left: var(--block-border) solid 10px; border-radius: 3px; padding: 12px 16px; } .tag-block { padding: 16px; line-height: 1.75; background-color: var(--tag-block); border-radius: 6px; } .big-font { font-size: 1.35rem; margin-top: 1rem; } .top_container { margin-bottom: 2rem; } .top_container .top_content { line-height: 1.5; } ================================================ FILE: apps/examples/src/templates/clearButtonTemplate.ts ================================================ import CONSTANTS from '@/constants'; const { url, name, areasUrl } = CONSTANTS; const clearButtonTemplate = `import React, { Fragment } from 'react'; import ImageMapper from 'react-img-mapper'; const Mapper = () => { const url = '${url}'; const name = '${name}'; // Get JSON from below URL as an example and put it into the useState hook // URL: ${areasUrl} const initialAreas = []; const [areas, setAreas] = useState(initialAreas); return ( setAreas(newAreas)} isMulti /> ) } export default Mapper;`; export default clearButtonTemplate; ================================================ FILE: apps/examples/src/templates/variablesTemplate.ts ================================================ import CONSTANTS from '@/constants'; const { url, name, areasUrl } = CONSTANTS; const variablesTemplate = `const url = '${url}'; const name = '${name}'; // Get JSON from below URL as an example and put it here const areas = '${areasUrl}';`; export default variablesTemplate; ================================================ FILE: apps/examples/src/templates/zoomTemplate.ts ================================================ import variablesTemplate from '@/templates/variablesTemplate'; const zoomTemplate = `import React, { Fragment, useState } from 'react'; import ImageMapper from 'react-img-mapper'; const Mapper = props => { const minWidth = 400; const [zoom, setZoom] = useState(640); ${variablesTemplate} const handleZoom = type => { setZoom(prev => { if (prev <= minWidth && type === 'out') return prev; return type === 'in' ? prev + props.parentWidth : prev - props.parentWidth; }); }; return ( ) } export default Mapper;`; export default zoomTemplate; ================================================ FILE: apps/examples/src/types/globals.d.ts ================================================ import 'react-img-mapper'; declare module 'react-img-mapper' { interface OverrideMapArea { title: string; } } ================================================ FILE: apps/examples/src/types/index.ts ================================================ import type { FC, ReactNode } from 'react'; export interface Children { children: ReactNode; } export type Component = FC; export type Layout = Component; ================================================ FILE: apps/examples/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, "skipLibCheck": true, "jsx": "react-jsx", "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": [ "**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.mts", "**/*.d.ts", ".storybook/**/*.ts", ".storybook/**/*.tsx" ], "exclude": ["node_modules", "dist"] } ================================================ FILE: apps/examples/vercel.json ================================================ { "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "pnpm build", "cleanUrls": true, "devCommand": "pnpm dev", "framework": "storybook", "installCommand": "pnpm install", "outputDirectory": "storybook-static" } ================================================ FILE: docs/.vitepress/config.mts ================================================ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitepress'; import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'; import type { Plugin } from 'vitepress'; const projectRoot = fileURLToPath(new URL('../..', import.meta.url)); const { version } = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')); const githubUrl = 'https://github.com/img-mapper/img-mapper'; const npmUrl = 'https://www.npmjs.com/package/react-img-mapper'; const siteUrl = 'https://img-mapper.nishargshah.dev'; const exampleUrl = 'https://img-mapper-examples.nishargshah.dev'; const title = 'Img Mapper'; const description = 'A React/Vue Component for Creating Interactive and Highlighted Zones on Images'; export default defineConfig({ title, description, cleanUrls: true, lastUpdated: true, rewrites: {}, markdown: { config(md) { md.use(groupIconMdPlugin); }, }, vite: { plugins: [groupIconVitePlugin() as Plugin], }, sitemap: { hostname: siteUrl, }, themeConfig: { logo: '/logo.png', nav: [ { text: 'Guide', link: '/', }, { text: 'Examples', link: exampleUrl, }, { text: `v${version}`, items: [ { text: 'Release Notes', link: `${githubUrl}/releases`, }, { text: 'Changelog', link: `${githubUrl}/blob/master/CHANGELOG.md`, }, { text: 'Contributing', link: '/contribute/guide', }, ], }, ], sidebar: [ { text: 'Introduction', items: [ { text: 'Getting Started', link: '/guide/getting-started', }, { text: 'Examples', link: '/guide/examples', }, ], }, { text: 'React', collapsed: false, items: [ { text: 'Installation', link: '/react/installation', }, { text: 'Properties', link: '/react/properties', }, ], }, { text: 'Vue', collapsed: false, items: [ { text: 'Installation', link: '/vue/installation', }, { text: 'Properties', link: '/vue/properties', }, ], }, { text: 'Contribute', items: [ { text: 'Contributing', link: '/contribute/guide', }, ], }, ], socialLinks: [ { icon: 'github', link: githubUrl, }, { icon: 'npm', link: npmUrl, }, ], footer: { message: 'Released under the MIT License.', copyright: 'Copyright © 2025-PRESENT
Made with ❤️ by Nisharg Shah', }, editLink: { pattern: `${githubUrl}/edit/master/docs/:path`, text: 'Edit this page on GitHub', }, lastUpdated: { text: 'Last Updated on', formatOptions: { dateStyle: 'medium', timeStyle: 'short', hour12: true, }, }, search: { provider: 'local', options: { detailedView: true, }, }, }, head: [ ['link', { rel: 'icon', href: '/logo.png', type: 'image/png' }], ['meta', { name: 'theme-color', content: '#ffffff' }], ['meta', { name: 'author', content: `${title} Team` }], [ 'meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0, viewport-fit=cover' }, ], [ 'meta', { name: 'description', content: description, }, ], [ 'meta', { name: 'keywords', content: 'img-mapper, image-mapper, react-img-mapper, react-image-mapper, vue-img-mapper, vue-image-mapper, img mapper, image mapper, react img mapper, react image mapper, vue img mapper, vue image mapper', }, ], // OG ['meta', { property: 'og:title', content: title }], [ 'meta', { property: 'og:description', content: description, }, ], [ 'meta', { property: 'og:image', content: `${siteUrl}/og-logo.png`, }, ], ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:url', content: siteUrl }], ['meta', { property: 'og:site_name', content: title }], // TWITTER ['meta', { name: 'twitter:title', content: title }], [ 'meta', { name: 'twitter:description', content: description, }, ], [ 'meta', { name: 'twitter:image', content: `${siteUrl}/og-logo.png`, }, ], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:creator', content: '@iamnisharg' }], ], }); ================================================ FILE: docs/.vitepress/theme/index.ts ================================================ import { inject } from '@vercel/analytics'; // eslint-disable-next-line import-x/no-unresolved import 'virtual:group-icons.css'; import Theme from 'vitepress/theme'; import './style.css'; export default { extends: Theme, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types enhanceApp() { if (globalThis.window !== undefined) { inject(); } }, }; ================================================ FILE: docs/.vitepress/theme/style.css ================================================ /** * Theme * -------------------------------------------------------------------------- */ :root { --vp-c-brand-1: #00acc1; } .VPImage.image-src { margin-top: 2rem; } /** * Component: Home * -------------------------------------------------------------------------- */ :root { --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient(90deg, var(--vp-c-brand-1), #a67cff); --vp-home-hero-image-background-image: linear-gradient(160deg, #00bdd680, #8c24a880); --vp-home-hero-image-filter: blur(40px); } .dark { --vp-home-hero-image-background-image: linear-gradient(160deg, var(--vp-c-brand-1), #8e24aa); } @media (min-width: 640px) { :root { --vp-home-hero-image-filter: blur(56px); } } @media (min-width: 960px) { :root { --vp-home-hero-image-filter: blur(72px); } } /** * Component: Button * -------------------------------------------------------------------------- */ :root { --vp-button-brand-bg: var(--vp-c-brand-1); --vp-button-brand-hover-bg: #0097a7; --vp-button-brand-active-bg: #00838f; } .dark { --vp-button-brand-bg: var(--vp-c-brand-1); --vp-button-brand-hover-bg: #26c6da; --vp-button-brand-active-bg: #4dd0e1; } ================================================ FILE: docs/contribute/guide.md ================================================ # Contributing {#contributing} Thank you for considering contributing to `img-mapper`. We welcome all contributions, whether it’s fixing a bug, improving documentation, or suggesting new rules. ## How to Contribute {#how-to-contribute} ### 1. Fork & Clone the Repository {#for-clone-repository} ::: code-group ```sh [SSH] $ git clone git@github.com:img-mapper/img-mapper.git $ cd img-mapper ``` ```sh [HTTPS] $ git clone https://github.com/img-mapper/img-mapper.git $ cd img-mapper ``` ::: ### 2. Install Dependencies {#install-dependencies} Check the `.nvmrc` file for the required Node.js version. For `pnpm` version, see the `packageManager` field in the root `package.json`. This project is a **monorepo** managed with **pnpm**. Install dependencies with: ```sh $ pnpm install ``` ### 3. Project Structure {#project-structure} The repo is organized as a monorepo with two main packages: - `packages/react-img-mapper` → React img mapper package - `packages/vue-img-mapper` → Vue img mapper package - `apps/examples` → Img mapper examples - `docs/` → Documentation site (built with VitePress) ### 4. Making Changes {#making-changes} - Always create a new branch: ```sh $ git checkout -b fix/your-change ``` - For docs → check formatting and verify links. ### 5. Linting & Formatting {#linting-formatting} Run checks and fixes before committing: ::: code-group ```sh [Check] $ pnpm lint $ pnpm format:check ``` ```sh [Fix] $ pnpm lint:fix $ pnpm format:fix ``` ::: ### 6. Commit Guidelines {#commit-guidelines} We follow **Conventional Commits** for a clean commit history. Examples: - `feat: implement ESM functionality` - `fix: resolve path alias issue` - `docs: update installation steps` ### 7. Running Scripts {#running-scripts} Before pushing, ensure all scripts pass: ```sh $ pnpm script:lint ``` ### 8. Submitting a PR {#submitting-pr} - Push your branch and open a Pull Request against `canary`. - Clearly describe the problem, your solution, and reference any related issues/discussions. - Maintainers will review, suggest improvements if needed, and merge once approved. ## Code of Conduct {#code-of-conduct} This project follows a [**Code of Conduct**](https://github.com/img-mapper/img-mapper/blob/master/CODE_OF_CONDUCT.md). Please be respectful, collaborative, and inclusive. ## Suggestions & Issues {#suggestions-issues} - Found a bug? → [Open an Issue](https://github.com/img-mapper/img-mapper/issues/new/choose) - Want a new feature or rule? → Use the same link to create an issue, or start a discussion before opening a PR. ================================================ FILE: docs/guide/examples.md ================================================ # Examples {#examples} Explore live demos of Img Mapper at: [**img-mapper-examples.nishargshah.dev**](https://img-mapper-examples.nishargshah.dev) ::: tip Use the site to quickly explore examples and see how interactive areas, events, and responsive scaling work in real time. ::: The examples site demonstrates **real-world use cases** of Img Mapper in both **React** and **Vue**. Each demo illustrates how to: - Define interactive image regions using JSON-based coordinate maps - Implement hover, click, and other interactions - Implement Multi-selection and toggle support for complex workflows - Customize styles with fill colors, strokes, and opacities - Ensure responsive behavior as images and maps scale across devices Whether you're building product hotspots, floor plans, or data visualizations, these examples provide practical insights into integrating Img Mapper into your projects. Each demo is **playable and editable**, giving you hands-on understanding of how to integrate Img Mapper into your own projects. ================================================ FILE: docs/guide/getting-started.md ================================================ # Getting Started {#getting-started} **Img Mapper** is an open-source library that enables developers to create **interactive, clickable, and highlightable regions on images**. It simplifies building visual interfaces like seat maps, product previews, floor plans, or educational diagrams, anywhere you want users to interact directly with parts of an image. The library provides a **declarative API** to define areas using coordinate data, manage hover and click events, and style highlighted zones. It is available for both **React** and **Vue**, ensuring a consistent experience across frameworks. ## React Img Mapper {#react-img-mapper} **React Img Mapper** is the React implementation of Img Mapper. It allows you to define and manage image map regions in React components with full event support. ### Key Features {#react-key-features} - Define clickable or hoverable zones with JSON coordinates - Handle events like `onClick`, `onMouseEnter`, and `onMouseLeave` - Customize colors, opacity, and borders for each area - Supports responsive resizing while preserving mapping accuracy ## Vue Img Mapper {#vue-img-mapper} **Vue Img Mapper** brings the same functionality to the Vue ecosystem with a native component syntax. ### Key Features {#vue-key-features} - Simple and reactive props for managing maps and areas - Emits events such as `click`, `mouseenter`, and `mouseleave` - Full compatibility with Vue 3 composition API - Lightweight and flexible and perfect for interactive UIs ================================================ FILE: docs/index.md ================================================ --- # https://vitepress.dev/reference/default-theme-home-page layout: home hero: name: 'Img Mapper' tagline: A React/Vue Component for Creating Interactive and Highlighted Zones on Images image: src: /logo.png alt: Image actions: - theme: brand text: Get Started link: /guide/getting-started - theme: alt text: React link: /react/installation - theme: alt text: Vue link: /vue/installation features: - icon: 🚀 title: Next.js & SSR Ready details: Works perfectly with Next.js and other SSR frameworks. - icon: 📱 title: Fully Responsive details: Automatically adapts to any screen size or container. - icon: 📦 title: TypeScript Support details: Out-of-the-box TypeScript support for all your code. - icon: 🪶 title: Lightweight & Fast details: Minimal bundle size for top-notch performance. - icon: ⚙️ title: Composable API details: Designed for flexibility and easy integration in React apps. - icon: 📘 title: Extensive Docs & Examples details: Learn quickly with guided usage and live demos. --- ================================================ FILE: docs/package.json ================================================ { "name": "img-mapper-docs", "version": "2.0.3", "private": true, "description": "Documentation of img-mapper", "homepage": "https://img-mapper.nishargshah.dev", "bugs": { "url": "https://github.com/img-mapper/img-mapper/issues" }, "repository": { "type": "git", "url": "https://github.com/img-mapper/img-mapper.git", "directory": "docs" }, "license": "MIT", "author": "Nisharg Shah ", "type": "module", "scripts": { "build": "vitepress build", "dev": "vitepress dev", "preview": "vitepress preview", "typecheck": "tsc --noEmit" }, "dependencies": { "@vercel/analytics": "catalog:" }, "devDependencies": { "typescript": "catalog:", "vitepress": "^1.6.4", "vitepress-plugin-group-icons": "^1.6.4" } } ================================================ FILE: docs/react/installation.md ================================================ # Installation {#installation} Install **react-img-mapper** using your preferred package manager: ::: code-group ```sh [npm] $ npm install react-img-mapper ``` ```sh [yarn] $ yarn add react-img-mapper ``` ```sh [pnpm] $ pnpm install react-img-mapper ``` ::: ## Usage Example Integrate `react-img-mapper` into your React app: ```javascript import React from 'react'; import ImageMapper from 'react-img-mapper'; const Mapper = () => { const url = 'https://img-mapper-examples.nishargshah.dev/assets/example.jpg'; const name = 'my-map'; // Get JSON from below URL as an example and put it here const areas = 'https://img-mapper-examples.nishargshah.dev/assets/areas.json'; return ; }; export default Mapper; ``` ================================================ FILE: docs/react/properties.md ================================================ # Properties {#properties} Together, below sections let you fully control the component, customize its behavior and appearance, handle user interactions, configure individual areas, and access internal function references via React refs. ## Component Properties {#component-properties} Configure the main behavior, appearance, and responsiveness of the component. | Prop | Type | Description | Default | | ---------------- | -------------------------- | ---------------------------------------------------------- | -------------------------- | | `src` | string | Image URL to display | **required** | | `name` | string | Unique map name associated with the image | **required** | | `areas` | array | Array of area objects (see **Area Properties**) | **required** | | `areaKeyName` | string | Key used to uniquely identify areas | `id` | | `isMulti` | bool | Allows multiple areas to be selected | `true` | | `toggle` | bool | Enables toggling selection on click | `false` | | `active` | bool | Enables area listeners and highlighting | `true` | | `disabled` | bool | Disables highlighting and interactions | `false` | | `fillColor` | string | Highlight fill color | `rgba(255, 255, 255, 0.5)` | | `strokeColor` | string | Highlight border color | `rgba(0, 0, 0, 0.5)` | | `lineWidth` | number | Border thickness of highlighted zones | `1` | | `imgWidth` | number | Original width of the image | `0` | | `width` | number \| (func => number) | Image width (can be use as a function for dynamic sizing) | `0` | | `height` | number \| (func => number) | Image height (can be use as a function for dynamic sizing) | `0` | | `natural` | bool | Use the image's original dimensions | `false` | | `responsive` | bool | Enable responsive scaling (requires `parentWidth`) | `false` | | `parentWidth` | number | Max width of parent container | `0` | | `containerProps` | object | Props for the wrapping `
` | `null` | | `imgProps` | object | Props for the `` element | `null` | | `canvasProps` | object | Props for the `` element | `null` | | `mapProps` | object | Props for the `` element | `null` | | `areaProps` | object \| array | Props for `` elements | `null` | ## Callbacks {#callbacks} Handle user interactions, such as clicks, hovers, and touch events on the mapped areas or image. | Callback | Trigger | Signature | | ------------------ | --------------------------------- | ------------------------------- | | `onChange` | Click on an area | `(selectedArea, areas) => void` | | `onImageClick` | Click outside mapped areas | `(event) => void` | | `onImageMouseMove` | Mouse move over the image | `(event) => void` | | `onClick` | Click on a mapped area | `(area, index, event) => void` | | `onMouseDown` | Mouse down on area | `(area, index, event) => void` | | `onMouseUp` | Mouse up on area | `(area, index, event) => void` | | `onTouchStart` | Touch start on area | `(area, index, event) => void` | | `onTouchEnd` | Touch end on area | `(area, index, event) => void` | | `onMouseMove` | Mouse move over area | `(area, index, event) => void` | | `onMouseEnter` | Hover over area | `(area, index, event) => void` | | `onMouseLeave` | Leave area | `(area, index, event) => void` | | `onLoad` | Image loaded & canvas initialized | `(event, dimensions) => void` | ## Methods {#methods} Retrieve internal function references through React refs for advanced control. | Method | Description | | --------- | ---------------------------------------------------------- | | `getRefs` | Returns refs for the container, canvas, and image elements | --- ## Area Properties {#area-properties} Define individual area shapes, coordinates, styling, and interaction behavior within the image map. | Property | Type | Description | Default | | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | | `id` | string | Unique identifier; defaults to index if not provided. This can be customized using the `areaKeyName` property. | based on `areaKeyName` | | `shape` | string | Shape: `rect`, `circle`, `poly` | **required** | | `coords` | string[] | Coordinates for the area:
  • **rect**: `top-left-X, top-left-Y, bottom-right-X, bottom-right-Y`
  • **circle**: `center-X, center-Y, radius`
  • **poly**: List of points defining the polygon as `point-X, point-Y, ...`
| **required** | | `active` | bool | Enables area listeners and highlighting | `true` | | `disabled` | bool | Disables highlighting and interactions | `false` | | `href` | string | Target link for area clicks, ignored if `onClick` exists | `undefined` | | `fillColor` | string | Highlight fill color | `rgba(255, 255, 255, 0.5)` | | `strokeColor` | string | Highlight border color | `rgba(0, 0, 0, 0.5)` | | `lineWidth` | number | Border thickness of highlighted zones | `1` | | `preFillColor` | string | Pre-filled highlight color | `undefined` | Additional properties available when triggered via an event: | Property | Type | Description | | -------------- | -------- | ------------------------------------------ | | `scaledCoords` | number[] | Coordinates adjusted to current image size | | `center` | number[] | Centroid coordinates `[X, Y]` of the area | ================================================ FILE: docs/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "composite": true, "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["./*"] } }, "include": [ "**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.mts", ".vitepress/**/*.ts", ".vitepress/**/*.mts" ], "exclude": ["node_modules", "dist"] } ================================================ FILE: docs/vercel.json ================================================ { "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "pnpm build", "cleanUrls": true, "devCommand": "pnpm dev", "framework": "vitepress", "installCommand": "pnpm install", "outputDirectory": ".vitepress/dist" } ================================================ FILE: docs/vue/installation.md ================================================ # Installation {#installation} ::: warning `vue-img-mapper` is currently in beta. Features and APIs are still evolving and improvements are coming soon. ::: Install **vue-img-mapper** using your preferred package manager: ::: code-group ```sh [npm] $ npm install vue-img-mapper ``` ```sh [yarn] $ yarn add vue-img-mapper ``` ```sh [pnpm] $ pnpm install vue-img-mapper ``` ::: ## Usage Example Integrate `vue-img-mapper` into your Vue app: ```javascript ``` ================================================ FILE: docs/vue/properties.md ================================================ # Properties {#properties} ::: warning `vue-img-mapper` is currently in beta. Features and APIs are still evolving and improvements are coming soon. ::: Together, below sections let you fully control the component, customize its behavior and appearance, handle user interactions, configure individual areas, and access internal function references via Vue refs. ## Component Properties {#component-properties} Configure the main behavior, appearance, and responsiveness of the component. | Prop | Type | Description | Default | | ---------------- | -------------------------- | ---------------------------------------------------------- | -------------------------- | | `src` | string | Image URL to display | **required** | | `name` | string | Unique map name associated with the image | **required** | | `areas` | array | Array of area objects (see **Area Properties**) | **required** | | `areaKeyName` | string | Key used to uniquely identify areas | `id` | | `isMulti` | bool | Allows multiple areas to be selected | `true` | | `toggle` | bool | Enables toggling selection on click | `false` | | `active` | bool | Enables area listeners and highlighting | `true` | | `disabled` | bool | Disables highlighting and interactions | `false` | | `fillColor` | string | Highlight fill color | `rgba(255, 255, 255, 0.5)` | | `strokeColor` | string | Highlight border color | `rgba(0, 0, 0, 0.5)` | | `lineWidth` | number | Border thickness of highlighted zones | `1` | | `imgWidth` | number | Original width of the image | `0` | | `width` | number \| (func => number) | Image width (can be use as a function for dynamic sizing) | `0` | | `height` | number \| (func => number) | Image height (can be use as a function for dynamic sizing) | `0` | | `natural` | bool | Use the image's original dimensions | `false` | | `responsive` | bool | Enable responsive scaling (requires `parentWidth`) | `false` | | `parentWidth` | number | Max width of parent container | `0` | | `containerProps` | object | Props for the wrapping `
` | `null` | | `imgProps` | object | Props for the `` element | `null` | | `canvasProps` | object | Props for the `` element | `null` | | `mapProps` | object | Props for the `` element | `null` | | `areaProps` | object \| array | Props for `` elements | `null` | ## Callbacks {#callbacks} Handle user interactions, such as clicks, hovers, and touch events on the mapped areas or image. | Callback | Trigger | Signature | | ----------------- | --------------------------------- | ------------------------------- | | `@change` | Click on an area | `(selectedArea, areas) => void` | | `@imageClick` | Click outside mapped areas | `(event) => void` | | `@imageMouseMove` | Mouse move over the image | `(event) => void` | | `@click` | Click on a mapped area | `(area, index, event) => void` | | `@mousedown` | Mouse down on area | `(area, index, event) => void` | | `@mouseup` | Mouse up on area | `(area, index, event) => void` | | `@touchstart` | Touch start on area | `(area, index, event) => void` | | `@touchend` | Touch end on area | `(area, index, event) => void` | | `@mousemove` | Mouse move over area | `(area, index, event) => void` | | `@mouseenter` | Hover over area | `(area, index, event) => void` | | `@mouseleave` | Leave area | `(area, index, event) => void` | | `@load` | Image loaded & canvas initialized | `(event, dimensions) => void` | ## Methods {#methods} Retrieve internal function references through Vue refs for advanced control. | Method | Description | | --------- | ---------------------------------------------------------- | | `getRefs` | Returns refs for the container, canvas, and image elements | --- ## Area Properties {#area-properties} Define individual area shapes, coordinates, styling, and interaction behavior within the image map. | Property | Type | Description | Default | | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | | `id` | string | Unique identifier; defaults to index if not provided. This can be customized using the `areaKeyName` property. | based on `areaKeyName` | | `shape` | string | Shape: `rect`, `circle`, `poly` | **required** | | `coords` | string[] | Coordinates for the area:
  • **rect**: `top-left-X, top-left-Y, bottom-right-X, bottom-right-Y`
  • **circle**: `center-X, center-Y, radius`
  • **poly**: List of points defining the polygon as `point-X, point-Y, ...`
| **required** | | `active` | bool | Enables area listeners and highlighting | `true` | | `disabled` | bool | Disables highlighting and interactions | `false` | | `href` | string | Target link for area clicks, ignored if `onClick` exists | `undefined` | | `fillColor` | string | Highlight fill color | `rgba(255, 255, 255, 0.5)` | | `strokeColor` | string | Highlight border color | `rgba(0, 0, 0, 0.5)` | | `lineWidth` | number | Border thickness of highlighted zones | `1` | | `preFillColor` | string | Pre-filled highlight color | `undefined` | Additional properties available when triggered via an event: | Property | Type | Description | | -------------- | -------- | ------------------------------------------ | | `scaledCoords` | number[] | Coordinates adjusted to current image size | | `center` | number[] | Centroid coordinates `[X, Y]` of the area | ================================================ FILE: eslint.config.mjs ================================================ import customGeneralESLintConfig from './lint/general.eslint.mjs'; import customImportESLintConfig from './lint/import.eslint.mjs'; import customJSESLintConfig from './lint/javascript.eslint.mjs'; import customPrettierESLintConfig from './lint/prettier.eslint.mjs'; import customReactESLintConfig from './lint/react.eslint.mjs'; import customTSESLintConfig from './lint/typescript.eslint.mjs'; import { gitIgnoreFile } from './lint/utils.eslint.mjs'; export default [ gitIgnoreFile, ...customJSESLintConfig, ...customReactESLintConfig, ...customTSESLintConfig, ...customImportESLintConfig, ...customGeneralESLintConfig, ...customPrettierESLintConfig, ]; ================================================ FILE: lint/general.eslint.mjs ================================================ const customGeneralESLintConfig = [ { name: 'x/general/rules', rules: { 'no-console': 'off', 'no-void': 'off', 'consistent-return': 'off', 'no-array-constructor': 'off', 'no-underscore-dangle': [ 'error', { allow: ['_id'], }, ], 'no-restricted-syntax': [ 'error', 'ForStatement', 'ContinueStatement', 'DoWhileStatement', 'WhileStatement', 'WithStatement', // React { selector: 'MemberExpression[object.name="React"]', message: 'Use of React.method is not allowed.', }, // React - TypeScript { selector: 'TSTypeReference[typeName.left.name="React"]', message: 'Use of React.type is not allowed.', }, ], }, }, { name: 'x/general/ts-only', files: ['**/*.{ts,cts,mts,tsx}'], ignores: ['docs/**/*.{ts,cts,mts,tsx}'], rules: { 'no-restricted-imports': [ 'error', { patterns: [ { group: ['./*', '../*'], message: "Please use the absolute path '@/*' instead.", }, ], }, ], }, }, ]; export default customGeneralESLintConfig; ================================================ FILE: lint/import.eslint.mjs ================================================ import { rules } from 'eslint-config-airbnb-extended'; import unusedImportsPlugin from 'eslint-plugin-unused-imports'; const customImportESLintConfig = [ // Strict Import Rules rules.base.importsStrict, // Unused Import Config { name: 'unused-imports/config', plugins: { 'unused-imports': unusedImportsPlugin, }, rules: { 'unused-imports/no-unused-imports': 'error', }, }, // Disable Default Export for Hooks { name: 'x/import-x/disable-default-export', files: ['**/use*.ts'], rules: { 'import-x/prefer-default-export': 'off', }, }, // Disable Dependencies Import Issue for Templates ESLint Files { name: 'x/import-x/disable-extraneous-deps', files: ['docs/**/*.{ts,cts,mts,tsx}'], rules: { 'import-x/no-extraneous-dependencies': 'off', }, }, // Disable Extensions in Module Files { name: 'x/import-x/disable-extensions-in-module-files', files: ['**/*.mjs'], rules: { 'import-x/extensions': 'off', }, }, ]; export default customImportESLintConfig; ================================================ FILE: lint/javascript.eslint.mjs ================================================ import js from '@eslint/js'; import { configs, plugins } from 'eslint-config-airbnb-extended'; import promisePlugin from 'eslint-plugin-promise'; import unicornPlugin from 'eslint-plugin-unicorn'; const customJSESLintConfig = [ // ESLint Recommended Rules { name: 'js/config', ...js.configs.recommended, }, // Stylistic Plugin plugins.stylistic, // Import X Plugin plugins.importX, // Airbnb Base Recommended Config ...configs.base.recommended, // Promise Config promisePlugin.configs['flat/recommended'], // Unicorn Config unicornPlugin.configs.recommended, // Unicorn Config Rules { name: 'x/unicorn/rules', rules: { 'unicorn/filename-case': [ 'error', { cases: { kebabCase: true, camelCase: true, pascalCase: true, }, multipleFileExtensions: false, }, ], 'unicorn/prevent-abbreviations': 'off', 'unicorn/no-null': 'off', 'unicorn/no-array-reduce': 'off', 'unicorn/consistent-function-scoping': 'off', }, }, ]; export default customJSESLintConfig; ================================================ FILE: lint/prettier.eslint.mjs ================================================ import { rules as prettierConfigRules } from 'eslint-config-prettier'; import prettierPlugin from 'eslint-plugin-prettier'; const customPrettierESLintConfig = [ // Prettier Plugin { name: 'prettier/plugin/config', plugins: { prettier: prettierPlugin, }, }, // Prettier Config { name: 'prettier/config', rules: { ...prettierConfigRules, 'prettier/prettier': 'error', }, }, ]; export default customPrettierESLintConfig; ================================================ FILE: lint/react.eslint.mjs ================================================ import { configs, plugins, rules } from 'eslint-config-airbnb-extended'; const customReactESLintConfig = [ // React Plugin plugins.react, // React Hooks Plugin plugins.reactHooks, // React JSX A11y Plugin plugins.reactA11y, // Airbnb Next Recommended Config ...configs.react.recommended, // Airbnb React Strict Rules rules.react.strict, // JSX A11y Config Rules { name: 'x/jsx-a11y/rules', rules: { 'jsx-a11y/label-has-associated-control': 'off', }, }, // Disable JSX Runtime rule { name: 'x/react/disable-jsx-runtime', rules: { 'react/jsx-uses-react': 'off', 'react/react-in-jsx-scope': 'off', }, }, ]; export default customReactESLintConfig; ================================================ FILE: lint/typescript.eslint.mjs ================================================ import { configs, plugins, rules } from 'eslint-config-airbnb-extended'; const customTSESLintConfig = [ // TypeScript ESLint Plugin plugins.typescriptEslint, // Airbnb Base TypeScript Config ...configs.base.typescript, // Airbnb Next TypeScript Config ...configs.react.typescript, // Airbnb TypeScript ESLint Strict Rules rules.typescript.typescriptEslintStrict, // Disable Return Type for Features Hook { name: 'x/typescript-eslint/features-hook-only', files: ['src/features/**/use*.ts'], rules: { '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, ]; export default customTSESLintConfig; ================================================ FILE: lint/utils.eslint.mjs ================================================ import path from 'node:path'; import { includeIgnoreFile } from '@eslint/compat'; export const gitignorePath = path.resolve('.', '.gitignore'); export const gitIgnoreFile = includeIgnoreFile(gitignorePath); ================================================ FILE: lint-staged.config.mjs ================================================ /** * @type {import('lint-staged').Configuration} */ export default { '*.{js,mjs,jsx,ts,mts,tsx}': 'pnpm lint', }; ================================================ FILE: package.json ================================================ { "name": "img-mapper-project", "version": "2.0.3", "description": "Creates Interactive and Highlighted Zones on Images", "bugs": { "url": "https://github.com/img-mapper/img-mapper/issues" }, "repository": { "type": "git", "url": "https://github.com/img-mapper/img-mapper.git" }, "license": "MIT", "author": "Nisharg Shah ", "scripts": { "build": "pnpm -r build", "build:react": "pnpm --filter react-img-mapper build", "clean:all": "pnpm clean:node_modules && pnpm clean:generated-folders", "clean:dist": "pnpx rimraf dist && pnpm -r exec pnpx rimraf dist", "clean:generated-folders": "pnpm clean:dist", "clean:node_modules": "pnpx rimraf node_modules && pnpm -r exec pnpx rimraf node_modules", "clean:workspace": "pnpm clean:node_modules && pnpx rimraf pnpm-lock.yaml", "dev:docs": "pnpm --filter img-mapper-docs dev", "dev:examples": "pnpm --filter examples dev", "format:check": "prettier . --check", "format:fix": "prettier . --write", "fresh:init": "pnpm clean:workspace && pnpm clean:generated-folders && pnpm install", "lint": "eslint .", "lint:fix": "pnpm --silent lint --fix", "pkg:prepare": "pnpm changeset && pnpm changeset version", "pkg:publish": "pnpm build:react && pnpm changeset publish", "play:react": "pnpm --filter react-img-mapper play", "play:vue": "pnpm --filter vue-img-mapper play", "prepare": "husky || true", "script:lint": "bash -e ./scripts/lint.sh", "typecheck": "pnpm -r typecheck" }, "devDependencies": { "@changesets/cli": "^2.29.7", "@eslint/compat": "^1.4.0", "@eslint/js": "^9.38.0", "@stylistic/eslint-plugin": "^3.1.0", "@types/node": "^24.9.1", "eslint": "^9.38.0", "eslint-config-airbnb-extended": "^2.3.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-unicorn": "^61.0.2", "eslint-plugin-unused-imports": "^4.3.0", "husky": "^9.1.7", "lint-staged": "^16.2.6", "prettier": "^3.6.2", "prettier-plugin-packagejson": "^2.5.19", "typescript": "catalog:", "typescript-eslint": "^8.46.2" }, "packageManager": "pnpm@10.19.0", "engines": { "node": ">=16.0.0", "npm": "please-use-pnpm", "pnpm": ">=10.18.0", "yarn": "please-use-pnpm" } } ================================================ FILE: packages/react-img-mapper/.npmignore ================================================ # Ignore everything /* # Not ignored folders !dist # Inside dist *.tsbuildinfo ================================================ FILE: packages/react-img-mapper/README.md ================================================ # `react-img-mapper` [![NPM Version](https://img.shields.io/npm/v/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/react-img-mapper) [![NPM Downloads](https://img.shields.io/npm/dw/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/react-img-mapper) [![NPM Last Update](https://img.shields.io/npm/last-update/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/react-img-mapper) A React Component for Creating Interactive and Highlighted Zones on Images > Check out the package docs here: https://img-mapper.nishargshah.dev/react/installation ================================================ FILE: packages/react-img-mapper/package.json ================================================ { "name": "react-img-mapper", "version": "2.0.3", "description": "A React Component for Creating Interactive and Highlighted Zones on Images", "keywords": [ "react-img-mapper", "react-image-mapper", "img-mapper", "image-mapper", "img mapper", "image mapper", "react img mapper", "react image mapper", "react img mapper docs", "react image mapper docs" ], "homepage": "https://img-mapper.nishargshah.dev", "bugs": { "url": "https://github.com/img-mapper/img-mapper/issues" }, "repository": { "type": "git", "url": "https://github.com/img-mapper/img-mapper.git", "directory": "packages/react-img-mapper" }, "license": "MIT", "author": "Nisharg Shah ", "type": "module", "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.cts", "scripts": { "build": "tsdown", "dev": "tsdown --watch", "play": "vite --port 3000", "typecheck": "tsc --noEmit" }, "dependencies": { "react-fast-compare": "^3.2.2" }, "devDependencies": { "@types/react": "catalog:", "@types/react-dom": "catalog:", "@vitejs/plugin-react": "^5.0.4", "react": "catalog:", "tsdown": "catalog:", "typescript": "catalog:", "vite": "catalog:" }, "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" }, "engines": { "node": ">=16.0.0" } } ================================================ FILE: packages/react-img-mapper/playground/index.html ================================================ Playground
================================================ FILE: packages/react-img-mapper/playground/src/ReactPlayground.tsx ================================================ import React, { useEffect, useRef, useState } from 'react'; import ImageMapper from '@/ImageMapper'; import { useAreas } from '@playground/hooks/useAreas'; import type { FC } from 'react'; import type { MapArea, RefProperties } from '@/@types'; const name = 'my-map'; const url = 'https://img-mapper-examples.nishargshah.dev/assets/example.jpg'; const ReactPlayground: FC = () => { const { areas: initialAreas } = useAreas(); const [areas, setAreas] = useState(initialAreas); const [parentWidth, setParentWidth] = useState(640); const [responsive, setResponsive] = useState(false); const ref = useRef(null); useEffect(() => { if (ref.current) { console.log(ref.current.getRefs()); } }, []); const handleClick = () => { const area = areas.map((cur: MapArea, i: number) => { if (i % 4 === 0) { const temp = { ...cur }; temp.preFillColor = 'red'; return temp; } return cur; }); setAreas(area); }; useEffect(() => { if (areas.length === 0) setAreas(initialAreas); }, [initialAreas, areas.length]); if (areas.length === 0) return null; return ( console.log('onLoad =>>>>>>>>>>>>', arg)} parentWidth={responsive ? parentWidth : 0} responsive={responsive} src={url} onChange={(selectedArea, allAreas) => { console.log(selectedArea, allAreas); setAreas(allAreas); }} /> setParentWidth(e.target.valueAsNumber)} step={40} type="range" value={parentWidth} /> ); }; export default ReactPlayground; ================================================ FILE: packages/react-img-mapper/playground/src/hooks/useAreas.ts ================================================ import { useEffect, useState } from 'react'; import type { MapArea } from 'react-img-mapper'; interface AreasHookOutput { areas: MapArea[]; } type AreasHook = () => AreasHookOutput; const areasUrl = 'https://img-mapper-examples.nishargshah.dev/assets/areas.json'; export const useAreas: AreasHook = () => { const [areas, setAreas] = useState([]); useEffect(() => { (async () => { const res = await fetch(areasUrl); const json = await res.json(); setAreas(json); })(); }, []); return { areas }; }; ================================================ FILE: packages/react-img-mapper/playground/src/main.tsx ================================================ import { createRoot } from 'react-dom/client'; import ReactPlayground from '@playground/ReactPlayground'; createRoot(document.querySelector('#root') as Element).render(); ================================================ FILE: packages/react-img-mapper/src/@types/area.d.ts ================================================ import type { Area, ImageMapperProps, MapArea } from '@/@types'; import type { GetPropDimensionParams } from '@/@types/dimensions'; type ScaleCoordsParams = GetPropDimensionParams & Pick, 'responsive' | 'parentWidth' | 'imgWidth'>; export type ScaleCoords = ( coords: MapArea['coords'], scaleCoordsParams: ScaleCoordsParams, ) => number[]; export type ComputeCenter = ( shape: MapArea['shape'], scaleCoordsParams: ReturnType, ) => Area['center']; type GetExtendedAreaParams = Pick< Required, 'fillColor' | 'lineWidth' | 'strokeColor' >; export type GetExtendedArea = ( area: MapArea, scaleCoordsParams: ScaleCoordsParams, params: GetExtendedAreaParams, ) => Area; ================================================ FILE: packages/react-img-mapper/src/@types/constants.d.ts ================================================ import type { ImageMapperProps } from '@/@types'; type RequiredProps = 'src' | 'name' | 'areas'; export type ImageMapperDefaultProps = Omit; ================================================ FILE: packages/react-img-mapper/src/@types/dimensions.d.ts ================================================ import type { RefObject } from 'react'; import type { Dimension, ImageMapperProps, Refs, WidthHeight } from '@/@types'; type ImgRef = RefObject; export type GetDimension = (dimension: Dimension, img: ImgRef) => number; export interface GetPropDimensionParams { width: Dimension; height: Dimension; img: ImgRef; } export type GetPropDimension = (params: GetPropDimensionParams) => WidthHeight; type GetDimensionValuesParams = GetPropDimensionParams & Pick, 'responsive' | 'parentWidth' | 'natural'>; export type GetDimensionValues = ( type: 'width' | 'height', params: GetDimensionValuesParams, ) => number; type GetDimensionsParams = Omit; export type GetDimensions = (params: GetDimensionsParams) => WidthHeight; export interface PrevStateRef { parentWidth: number; width: number; height: number; } ================================================ FILE: packages/react-img-mapper/src/@types/draw.d.ts ================================================ import type { RefObject } from 'react'; import type { Area } from '@/@types'; export type CTX = RefObject; type DrawArea = 'scaledCoords' | 'fillColor' | 'lineWidth' | 'strokeColor'; export type DrawChosenShape = (area: Pick, ctx: CTX) => boolean; export type DrawShape = (area: Pick, ctx: CTX) => boolean; export type GetShape = (shape: Area['shape']) => DrawChosenShape | false; ================================================ FILE: packages/react-img-mapper/src/@types/events.d.ts ================================================ import type { AreaEvent, ImageEvent, ImageMapperProps, MapArea } from '@/@types'; export interface EventListenerParam { area: MapArea; index: number; } export type EventListenerProps = Pick & { cb?: (area: MapArea) => void; }; export type EventListener = ( params: EventListenerParam, props: EventListenerProps, ) => (event: E) => void; export type ImageEventListener = ( props: EventListenerProps, ) => (event: ImageEvent) => void; ================================================ FILE: packages/react-img-mapper/src/@types/index.d.ts ================================================ import type { HTMLProps, MouseEvent, Ref, TouchEvent as ReactTouchEvent } from 'react'; import type { ConditionalKeys, NoUndefinedField } from '@/@types/lib'; export interface Refs { containerRef: HTMLDivElement | null; imgRef: HTMLImageElement | null; canvasRef: HTMLCanvasElement | null; } export interface RefProperties { getRefs: () => Refs; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface OverrideMapArea {} export interface MapArea extends OverrideMapArea { id: string; shape: string; coords: number[]; active?: boolean; disabled?: boolean; href?: string; fillColor?: string; strokeColor?: string; lineWidth?: number; preFillColor?: string; } type RequiredMapArea = 'active' | 'fillColor' | 'lineWidth' | 'strokeColor'; type RequiredArea = Omit & Pick, R>; export interface Area extends RequiredArea { scaledCoords: number[]; center: [number, number]; } export interface WidthHeight { width: number; height: number; } export type Dimension = number | ((event: HTMLImageElement) => number); export type ContainerProps = Omit, 'ref' | 'id'> | null; export type ImgProps = Omit< HTMLProps, 'ref' | 'src' | 'useMap' | 'onClick' | 'onMouseMove' > | null; export type CanvasProps = Omit, 'ref'> | null; export type MapProps = Omit, 'name'> | null; export type AreaProps = Omit< HTMLProps, | 'key' | 'coords' | 'onMouseEnter' | 'onMouseLeave' | 'onMouseMove' | 'onMouseDown' | 'onMouseUp' | 'onTouchStart' | 'onTouchEnd' | 'onClick' > | null; export type TouchEvent = ReactTouchEvent; export type AreaEvent = MouseEvent; export type ImageEvent = MouseEvent; export type ChangeEventHandler = ((selectedArea: MapArea, areas: MapArea[]) => void) | null; export type ImageEventHandler = ((event: ImageEvent) => void) | null; export type EventHandler = ((area: MapArea, index: number, e: T) => void) | null; export type LoadEventHandler = ((event: HTMLImageElement, dimensions: WidthHeight) => void) | null; export interface ImageMapperProps { src: string; name: string; areas: MapArea[]; areaKeyName?: ConditionalKeys; isMulti?: boolean; toggle?: boolean; active?: boolean; disabled?: boolean; fillColor?: string; strokeColor?: string; lineWidth?: number; imgWidth?: number; width?: Dimension; height?: Dimension; natural?: boolean; responsive?: boolean; parentWidth?: number; containerProps?: ContainerProps; imgProps?: ImgProps; canvasProps?: CanvasProps; mapProps?: MapProps; areaProps?: AreaProps | AreaProps[]; onChange?: ChangeEventHandler; onImageClick?: ImageEventHandler; onImageMouseMove?: ImageEventHandler; onClick?: EventHandler; onMouseDown?: EventHandler; onMouseUp?: EventHandler; onTouchStart?: EventHandler; onTouchEnd?: EventHandler; onMouseMove?: EventHandler; onMouseEnter?: EventHandler; onMouseLeave?: EventHandler; onLoad?: LoadEventHandler; } export interface ImageMapperPropsWithRef extends ImageMapperProps { ref?: Ref; } ================================================ FILE: packages/react-img-mapper/src/@types/lib.d.ts ================================================ export type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> }; export type ConditionalKeys = NonNullable< { [Key in keyof Base]: Base[Key] extends Condition ? Key : never; }[keyof Base] >; ================================================ FILE: packages/react-img-mapper/src/@types/styles.d.ts ================================================ import type { CSSProperties } from 'react'; export interface StylesProps { container: CSSProperties; canvas: CSSProperties; img: (responsive: boolean) => CSSProperties; map: (onClick: boolean) => CSSProperties | undefined; } ================================================ FILE: packages/react-img-mapper/src/ImageMapper.tsx ================================================ import React, { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import isEqual from 'react-fast-compare'; import { getExtendedArea } from '@/helpers/area'; import { generateProps, rerenderPropsList } from '@/helpers/constants'; import { getDimension, getDimensions, getPropDimension } from '@/helpers/dimensions'; import drawShape from '@/helpers/draw'; import { click, imageClick, imageMouseMove, mouseDown, mouseEnter, mouseLeave, mouseMove, mouseUp, touchEnd, touchStart, } from '@/helpers/events'; import styles from '@/helpers/styles'; import type { FC, ReactNode } from 'react'; import type { ImageMapperPropsWithRef, MapArea, Refs } from '@/@types'; import type { PrevStateRef } from '@/@types/dimensions'; import type { CTX } from '@/@types/draw'; const ImageMapper: FC = ({ ref, ...props }) => { const generatedProps = generateProps(props); const { src, name, areas, areaKeyName, isMulti, toggle, active, disabled, fillColor, strokeColor, lineWidth, imgWidth, width, height, natural, responsive, parentWidth, containerProps, imgProps, canvasProps, mapProps, areaProps, onChange, onImageClick, onImageMouseMove, onClick, onMouseDown, onMouseUp, onTouchStart, onTouchEnd, onMouseMove, onMouseEnter, onMouseLeave, onLoad, } = generatedProps; const [isRendered, setIsRendered] = useState(false); const areasRef = useRef(areas); const containerRef = useRef(null); const img = useRef(null); const canvas = useRef(null); const ctx = useRef['current']>(null); const interval = useRef(0); const prevState = useRef({ parentWidth, ...getPropDimension({ width, height, img }), }); const dimensionParams = useMemo( () => ({ width, height, responsive, parentWidth, natural }), [width, height, responsive, parentWidth, natural], ); const scaleCoordsParams = useMemo( () => ({ width, height, responsive, parentWidth, imgWidth }), [width, height, responsive, parentWidth, imgWidth], ); const areaParams = useMemo( () => ({ fillColor, lineWidth, strokeColor }), [fillColor, lineWidth, strokeColor], ); const init = useCallback(() => { if (img.current?.complete && canvas.current && containerRef.current) { ctx.current = canvas.current.getContext('2d'); setIsRendered(true); } }, []); useEffect(() => { if (isRendered) { clearInterval(interval.current); } else { // eslint-disable-next-line unicorn/prefer-global-this interval.current = window.setInterval(init, 500); } }, [init, isRendered]); const renderPrefilledAreas = useCallback(() => { // eslint-disable-next-line unicorn/no-array-for-each areas.forEach((area) => { const extendedArea = getExtendedArea(area, { img, ...scaleCoordsParams }, areaParams); if (!extendedArea.preFillColor) return false; return drawShape({ ...extendedArea, fillColor: extendedArea.preFillColor }, ctx); }); }, [areaParams, areas, scaleCoordsParams]); const clearCanvas = useCallback(() => { if (!(ctx.current && canvas.current)) return; ctx.current.clearRect(0, 0, canvas.current.width, canvas.current.height); }, []); const resetCanvasAndPrefillArea = useCallback(() => { clearCanvas(); renderPrefilledAreas(); }, [clearCanvas, renderPrefilledAreas]); const highlightArea = (area: MapArea): boolean => { const extendedArea = getExtendedArea(area, { img, ...scaleCoordsParams }, areaParams); if (!extendedArea.active) return false; return drawShape(extendedArea, ctx); }; const onHighlightArea = (area: MapArea): void => { const chosenAreasRef = areasRef.current; const chosenArea = isMulti ? area : chosenAreasRef.find((c) => c[areaKeyName] === area[areaKeyName]); if (!chosenArea) return; const extendedArea = getExtendedArea(chosenArea, { img, ...scaleCoordsParams }, areaParams); if (!(active && extendedArea.active)) return; const chosenAreas = isMulti ? areas : chosenAreasRef; const newArea = { ...chosenArea }; const isCurrentAreaSelected = (() => { if (toggle) { if (isMulti && newArea.preFillColor) return true; return !isMulti && !!area.preFillColor; } return false; })(); if (isCurrentAreaSelected) { const isPreFillColorFromJSON = chosenAreas.find((c) => c[areaKeyName] === area[areaKeyName]); if (isPreFillColorFromJSON?.preFillColor) delete newArea.preFillColor; } else { newArea.preFillColor = extendedArea.fillColor; } const updatedAreas = chosenAreas.map((cur) => cur[areaKeyName] === area[areaKeyName] ? newArea : cur, ); if (onChange) onChange(newArea, updatedAreas); }; const initCanvas = useCallback( (isFirstTime = true, triggerOnLoad = false) => { const { width: imageWidth, height: imageHeight } = getDimensions({ img, ...dimensionParams }); if (!(img.current && canvas.current && containerRef.current && ctx.current)) return; containerRef.current.style.width = `${imageWidth}px`; containerRef.current.style.height = `${imageHeight}px`; if (isFirstTime) { initCanvas(false, true); } else { img.current.width = imageWidth; img.current.height = imageHeight; canvas.current.width = imageWidth; canvas.current.height = imageHeight; renderPrefilledAreas(); } if (onLoad && triggerOnLoad) { onLoad(img.current, { width: imageWidth, height: imageHeight }); } }, [dimensionParams, onLoad, renderPrefilledAreas], ); const getRefs = useCallback( () => ({ containerRef: containerRef.current, imgRef: img.current, canvasRef: canvas.current }), [], ); useEffect(() => { if (isRendered) initCanvas(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRendered]); useEffect(() => { resetCanvasAndPrefillArea(); }, [areas, resetCanvasAndPrefillArea]); useEffect(() => { if (responsive && parentWidth && prevState.current.parentWidth !== parentWidth) { initCanvas(); prevState.current.parentWidth = parentWidth; } if (width && prevState.current.width !== width) { initCanvas(); prevState.current.width = getDimension(width, img); } if (height && prevState.current.height !== height) { initCanvas(); prevState.current.height = getDimension(height, img); } }, [height, initCanvas, parentWidth, responsive, width]); useImperativeHandle(ref, () => ({ getRefs }), [getRefs]); const handleMouseEnter = (area: MapArea): void => { if (active) highlightArea(area); }; const handleMouseLeave = (): void => { if (active) resetCanvasAndPrefillArea(); }; const handleClick = (area: MapArea): void => { onHighlightArea(area); }; const renderAreas = (): ReactNode => areas.map((area, index) => { const { areaKeyName: areaKeyNameProp } = props; const { scaledCoords } = getExtendedArea(area, { img, ...scaleCoordsParams }, areaParams); if (area.disabled) return null; const { preFillColor, shape, href } = area; const currentAreaProps = (() => { if (Array.isArray(areaProps)) { if (areaKeyNameProp) { return areaProps.find((cur) => cur && cur[areaKeyNameProp] === area[areaKeyNameProp]); } return areaProps[index]; } return areaProps; })(); return ( map ); }); return (
map {isRendered && !disabled ? renderAreas() : null}
); }; export default memo(ImageMapper, (prevProps, nextProps) => { const propChanged = rerenderPropsList.some((prop) => prevProps[prop] !== nextProps[prop]); return isEqual(prevProps.areas, nextProps.areas) && !propChanged; }); ================================================ FILE: packages/react-img-mapper/src/helpers/area.ts ================================================ import { getPropDimension } from '@/helpers/dimensions'; import type { ComputeCenter, GetExtendedArea, ScaleCoords } from '@/@types/area'; export const scaleCoords: ScaleCoords = ( coords, { width, height, img, responsive, parentWidth, imgWidth }, ) => coords.map((coord) => { if (responsive && parentWidth && img.current) { return coord / (img.current.naturalWidth / parentWidth); } const { width: imageWidth } = getPropDimension({ width, height, img }); const scale = imageWidth && imgWidth > 0 ? imageWidth / imgWidth : 1; return coord * scale; }); export const computeCenter: ComputeCenter = (shape, scaledCoords) => { switch (shape) { case 'circle': { return [scaledCoords[0], scaledCoords[1]]; } default: { const n = scaledCoords.length / 2; const { y: scaleY, x: scaleX } = scaledCoords.reduce( ({ y, x }, val, idx) => (idx % 2 ? { y: y + val / n, x } : { y, x: x + val / n }), { y: 0, x: 0 }, ); return [scaleX, scaleY]; } } }; export const getExtendedArea: GetExtendedArea = ( area, scaleCoordsParams, { fillColor, lineWidth, strokeColor }, ) => { const scaledCoords = scaleCoords(area.coords, scaleCoordsParams); const center = computeCenter(area.shape, scaledCoords); return { ...area, scaledCoords, center, active: area.active ?? true, fillColor: area.fillColor ?? fillColor, lineWidth: area.lineWidth ?? lineWidth, strokeColor: area.strokeColor ?? strokeColor, }; }; ================================================ FILE: packages/react-img-mapper/src/helpers/constants.ts ================================================ import type { ImageMapperProps } from '@/@types'; import type { ImageMapperDefaultProps } from '@/@types/constants'; export const rerenderPropsList = [ 'src', 'name', 'areaKeyName', 'isMulti', 'toggle', 'active', 'disabled', 'fillColor', 'strokeColor', 'lineWidth', 'imgWidth', 'width', 'height', 'natural', 'responsive', 'parentWidth', ] as const; const imageMapperDefaultProps: ImageMapperDefaultProps = { areaKeyName: 'id', isMulti: true, toggle: false, active: true, disabled: false, fillColor: 'rgba(255, 255, 255, 0.5)', strokeColor: 'rgba(0, 0, 0, 0.5)', lineWidth: 1, imgWidth: 0, width: 0, height: 0, natural: false, responsive: false, parentWidth: 0, containerProps: null, imgProps: null, canvasProps: null, mapProps: null, areaProps: null, onChange: null, onImageClick: null, onImageMouseMove: null, onClick: null, onMouseDown: null, onMouseUp: null, onTouchStart: null, onTouchEnd: null, onMouseMove: null, onMouseEnter: null, onMouseLeave: null, onLoad: null, }; export const generateProps = (props: T): Required => Object.entries(imageMapperDefaultProps).reduce( (acc, val) => { const [key, value] = val as unknown as [keyof T, typeof val]; // @ts-expect-error acc key error acc[key] = props[key] ?? value; return acc; }, { src: props.src, name: props.name, areas: props.areas }, ) as Required; ================================================ FILE: packages/react-img-mapper/src/helpers/dimensions.ts ================================================ import type { GetDimension, GetDimensions, GetDimensionValues, GetPropDimension, } from '@/@types/dimensions'; export const getDimension: GetDimension = (dimension, img) => { if (!img.current) return 0; return typeof dimension === 'function' ? dimension(img.current) : dimension; }; export const getPropDimension: GetPropDimension = ({ width, height, img }) => ({ width: getDimension(width, img), height: getDimension(height, img), }); const getDimensionValues: GetDimensionValues = ( type, { width, height, img, responsive, parentWidth, natural }, ) => { const { width: imageWidth, height: imageHeight } = getPropDimension({ width, height, img }); if (img.current) { const { naturalWidth, naturalHeight, clientWidth, clientHeight } = img.current; if (type === 'width') { if (responsive) return parentWidth; if (natural) return naturalWidth; if (imageWidth) return imageWidth; return clientWidth; } if (type === 'height') { if (responsive) return clientHeight; if (natural) return naturalHeight; if (imageHeight) return imageHeight; return clientHeight; } } return 0; }; export const getDimensions: GetDimensions = (props) => ({ width: getDimensionValues('width', props), height: getDimensionValues('height', props), }); ================================================ FILE: packages/react-img-mapper/src/helpers/draw.ts ================================================ import type { DrawChosenShape, DrawShape, GetShape } from '@/@types/draw'; const drawRect: DrawChosenShape = (area, ctx) => { const { scaledCoords, fillColor, lineWidth, strokeColor } = area; const [left, top, right, bottom] = scaledCoords; ctx.current.fillStyle = fillColor; ctx.current.lineWidth = lineWidth; ctx.current.strokeStyle = strokeColor; ctx.current.strokeRect(left, top, right - left, bottom - top); ctx.current.fillRect(left, top, right - left, bottom - top); return true; }; const drawCircle: DrawChosenShape = (area, ctx) => { const { scaledCoords, fillColor, lineWidth, strokeColor } = area; const [left, top, right] = scaledCoords; ctx.current.fillStyle = fillColor; ctx.current.beginPath(); ctx.current.lineWidth = lineWidth; ctx.current.strokeStyle = strokeColor; ctx.current.arc(left, top, right, 0, 2 * Math.PI); ctx.current.closePath(); ctx.current.stroke(); ctx.current.fill(); return true; }; const drawPoly: DrawChosenShape = (area, ctx) => { const { scaledCoords, fillColor, lineWidth, strokeColor } = area; const groupCoords = scaledCoords.reduce<[number, number][]>((acc, val, index, array) => { if (index % 2) return acc; return [...acc, array.slice(index, index + 2)] as [number, number][]; }, []); // const first = groupCoords.unshift(); ctx.current.fillStyle = fillColor; ctx.current.beginPath(); ctx.current.lineWidth = lineWidth; ctx.current.strokeStyle = strokeColor; // ctx.current.moveTo(first[0], first[1]); for (const [first, second] of groupCoords) ctx.current.lineTo(first, second); ctx.current.closePath(); ctx.current.stroke(); ctx.current.fill(); return true; }; const getShape: GetShape = (shape) => { if (shape === 'rect') return drawRect; if (shape === 'circle') return drawCircle; if (shape === 'poly') return drawPoly; return false; }; const drawShape: DrawShape = (area, ctx) => { const { shape, ...restArea } = area; const shapeFn = getShape(shape); if (shapeFn && ctx.current instanceof CanvasRenderingContext2D) { const currentCtx = { current: ctx.current }; return shapeFn(restArea, currentCtx); } return false; }; export default drawShape; ================================================ FILE: packages/react-img-mapper/src/helpers/events.ts ================================================ import type { TouchEvent } from '@/@types'; import type { EventListener, ImageEventListener } from '@/@types/events'; export const imageMouseMove: ImageEventListener<'onImageMouseMove'> = (props) => (event) => { const { onImageMouseMove } = props; if (onImageMouseMove) onImageMouseMove(event); }; export const imageClick: ImageEventListener<'onImageClick'> = (props) => (event) => { const { onImageClick } = props; if (onImageClick) { event.preventDefault(); onImageClick(event); } }; export const mouseEnter: EventListener<'onMouseEnter'> = ({ area, index }, props) => (event) => { const { onMouseEnter, cb } = props; if (cb) cb(area); if (onMouseEnter) onMouseEnter(area, index, event); }; export const mouseLeave: EventListener<'onMouseLeave'> = ({ area, index }, props) => (event) => { const { onMouseLeave, cb } = props; if (cb) cb(area); if (onMouseLeave) onMouseLeave(area, index, event); }; export const click: EventListener<'onClick'> = ({ area, index }, props) => (event) => { const { onClick, cb } = props; if (cb) cb(area); if (onClick) { event.preventDefault(); onClick(area, index, event); } }; export const mouseMove: EventListener<'onMouseMove'> = ({ area, index }, props) => (event) => { const { onMouseMove } = props; if (onMouseMove) onMouseMove(area, index, event); }; export const mouseDown: EventListener<'onMouseDown'> = ({ area, index }, props) => (event) => { const { onMouseDown } = props; if (onMouseDown) onMouseDown(area, index, event); }; export const mouseUp: EventListener<'onMouseUp'> = ({ area, index }, props) => (event) => { const { onMouseUp } = props; if (onMouseUp) onMouseUp(area, index, event); }; export const touchStart: EventListener<'onTouchStart', TouchEvent> = ({ area, index }, props) => (event) => { const { onTouchStart } = props; if (onTouchStart) onTouchStart(area, index, event); }; export const touchEnd: EventListener<'onTouchEnd', TouchEvent> = ({ area, index }, props) => (event) => { const { onTouchEnd } = props; if (onTouchEnd) onTouchEnd(area, index, event); }; ================================================ FILE: packages/react-img-mapper/src/helpers/styles.ts ================================================ import type { CSSProperties } from 'react'; import type { StylesProps } from '@/@types/styles'; const absPos: CSSProperties = { position: 'absolute', top: 0, left: 0, }; const imgNonResponsive: CSSProperties = { ...absPos, zIndex: 1, userSelect: 'none', }; const imgResponsive: CSSProperties = { ...imgNonResponsive, width: '100%', height: 'auto', }; const styles: StylesProps = { container: { position: 'relative', }, canvas: { ...absPos, pointerEvents: 'none', zIndex: 2, }, img: (responsive) => (responsive ? imgResponsive : imgNonResponsive), map: (onClick) => (onClick ? { cursor: 'pointer' } : undefined), }; export default styles; ================================================ FILE: packages/react-img-mapper/src/index.ts ================================================ export type * from '@/@types'; // eslint-disable-next-line no-restricted-exports export { default } from '@/ImageMapper'; ================================================ FILE: packages/react-img-mapper/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, "jsx": "react-jsx", "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@playground/*": ["./playground/src/*"] } }, "include": ["**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.mts", "**/*.d.ts"], "exclude": ["node_modules", "dist"] } ================================================ FILE: packages/react-img-mapper/tsdown.config.mts ================================================ // eslint-disable-next-line import-x/no-extraneous-dependencies import { defineConfig } from 'tsdown'; export default defineConfig((options) => { const { watch } = options; return { dts: true, format: ['cjs', 'esm'], outDir: 'dist', platform: 'browser', treeshake: !watch, minify: !watch, exports: !watch, }; }); ================================================ FILE: packages/react-img-mapper/vite.config.ts ================================================ import { fileURLToPath } from 'node:url'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; export default defineConfig({ root: './playground', plugins: [react()], resolve: { alias: { '@': fileURLToPath(new URL('src', import.meta.url)), '@playground': fileURLToPath(new URL('playground/src', import.meta.url)), }, }, }); ================================================ FILE: packages/vue-img-mapper/.npmignore ================================================ # Ignore everything /* # Not ignored folders !dist # Inside dist *.tsbuildinfo ================================================ FILE: packages/vue-img-mapper/README.md ================================================ # `vue-img-mapper` [![NPM Version](https://img.shields.io/npm/v/vue-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/vue-img-mapper) [![NPM Downloads](https://img.shields.io/npm/dw/vue-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/vue-img-mapper) [![NPM Last Update](https://img.shields.io/npm/last-update/vue-img-mapper?style=flat&labelColor=ffffff&color=00acc1)](https://www.npmjs.com/package/vue-img-mapper) A Vue Component for Creating Interactive and Highlighted Zones on Images > Check out the package docs here: https://img-mapper.nishargshah.dev/vue/installation ================================================ FILE: packages/vue-img-mapper/package.json ================================================ { "name": "vue-img-mapper", "version": "2.0.3", "description": "A Vue Component for Creating Interactive and Highlighted Zones on Images", "keywords": [ "vue-img-mapper", "vue-image-mapper", "img-mapper", "image-mapper", "img mapper", "image mapper", "vue img mapper", "vue image mapper", "vue img mapper docs", "vue image mapper docs" ], "homepage": "https://img-mapper.nishargshah.dev", "bugs": { "url": "https://github.com/img-mapper/img-mapper/issues" }, "repository": { "type": "git", "url": "https://github.com/img-mapper/img-mapper.git", "directory": "packages/vue-img-mapper" }, "license": "MIT", "author": "Nisharg Shah ", "type": "module", "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.cts", "scripts": { "build": "tsdown", "dev": "tsdown --watch", "play": "vite --port 3001", "typecheck": "vue-tsc --noEmit" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", "tsdown": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vue": "^3.5.22", "vue-tsc": "^3.1.1" }, "peerDependencies": { "vue": "^2.0.0 || ^3.0.0" }, "engines": { "node": ">=16.0.0" } } ================================================ FILE: packages/vue-img-mapper/playground/index.html ================================================ Playground
================================================ FILE: packages/vue-img-mapper/playground/src/App.vue ================================================ ================================================ FILE: packages/vue-img-mapper/playground/src/index.ts ================================================ import { createApp } from 'vue'; import App from '@playground/App.vue'; createApp(App).mount('#app'); ================================================ FILE: packages/vue-img-mapper/src/ImageMapper.vue ================================================ ================================================ FILE: packages/vue-img-mapper/src/helpers/area.ts ================================================ // eslint-disable-next-line import-x/prefer-default-export export const hello = (): void => { console.log('hello'); }; ================================================ FILE: packages/vue-img-mapper/src/index.ts ================================================ // eslint-disable-next-line no-restricted-exports export { default } from '@/ImageMapper.vue'; ================================================ FILE: packages/vue-img-mapper/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@playground/*": ["./playground/src/*"] } }, "include": ["**/*.ts", "**/*.mjs", "**/*.mts", "**/*.vue"], "exclude": ["node_modules", "dist"] } ================================================ FILE: packages/vue-img-mapper/tsdown.config.mts ================================================ // eslint-disable-next-line import-x/no-extraneous-dependencies import { defineConfig } from 'tsdown'; export default defineConfig((options) => { const { watch } = options; return { fromVite: true, dts: { vue: true, }, format: ['cjs', 'esm'], outDir: 'dist', platform: 'browser', treeshake: !watch, minify: !watch, exports: !watch, }; }); ================================================ FILE: packages/vue-img-mapper/vite.config.ts ================================================ import { fileURLToPath } from 'node:url'; import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; export default defineConfig({ root: './playground', plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('src', import.meta.url)), '@playground': fileURLToPath(new URL('playground/src', import.meta.url)), }, }, }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - docs - apps/* - packages/* catalog: '@types/react': ^19.2.2 '@types/react-dom': ^19.2.2 '@vercel/analytics': ^1.5.0 react: ^19.2.0 react-dom: ^19.2.0 tsdown: ^0.15.9 typescript: ^5.9.3 vite: ^7.1.11 onlyBuiltDependencies: - '@swc/core' - esbuild - unrs-resolver ================================================ FILE: prettier.config.mjs ================================================ /** * @see https://prettier.io/docs/configuration * @type {import("prettier").Config} */ export default { printWidth: 100, singleQuote: true, plugins: ['prettier-plugin-packagejson'], }; ================================================ FILE: scripts/lint.sh ================================================ #!/bin/bash mode="fix" filters=() for arg in "$@"; do case $arg in --for=*) mode="${arg#--for=}" ;; --filter=*) filterArg="${arg#--filter=}" IFS=',' read -ra userFilters <<<"$filterArg" for monorepo in "${userFilters[@]}"; do filters+=(--filter "$monorepo") done ;; esac done echo "Started (mode: $mode)" if [[ "$mode" == "ci" ]]; then pnpm "${filters[@]}" --silent format:check --log-level silent >/dev/null 2>&1 echo "Prettier Verified" pnpm "${filters[@]}" --silent lint >/dev/null 2>&1 echo "ESLint Verified" pnpm "${filters[@]}" --silent typecheck >/dev/null 2>&1 echo "TypeScript Verified" elif [[ "$mode" == "check" ]]; then pnpm "${filters[@]}" --silent format:check --log-level silent echo "Prettier Checked" pnpm "${filters[@]}" --silent lint echo "ESLint Checked" pnpm "${filters[@]}" --silent typecheck echo "TypeScript Checked" else pnpm "${filters[@]}" --silent format:fix --log-level silent echo "Prettier Completed" pnpm "${filters[@]}" --silent lint:fix echo "ESLint Completed" pnpm "${filters[@]}" --silent typecheck echo "TypeScript Completed" fi echo "Done" ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "preserve", "moduleResolution": "bundler", "lib": ["dom", "dom.iterable", "esnext"], "strict": true, "noImplicitReturns": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "resolveJsonModule": true, "useUnknownInCatchVariables": true, "isolatedModules": true, "declaration": true }, "files": [], "references": [ { "path": "apps/examples" }, { "path": "docs" }, { "path": "packages/react-img-mapper" }, { "path": "packages/vue-img-mapper" } ] }