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
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) => (
);
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:
handleZoom('in')} style={{ marginRight: 8 }} type="button">
Zoom In
handleZoom('out')} type="button">
Zoom Out
,
)
}
/>
);
};
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:
Clear
,
)
}
/>
),
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
/>
setAreas(initialAreas)}>Clear
)
}
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 (
handleZoom('in')}>Zoom In
handleZoom('out')}>Zoom Out
)
}
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`
[](https://www.npmjs.com/package/react-img-mapper)
[](https://www.npmjs.com/package/react-img-mapper)
[](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}
/>
Highlight
setAreas(initialAreas)} type="button">
Clear
setResponsive((prev) => !prev)} type="button">
{responsive ? 'Enabled: Responsive' : 'Enable: Responsive'}
console.log(ref.current?.getRefs())} type="button">
Get Ref
);
};
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 (
);
});
return (
{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`
[](https://www.npmjs.com/package/vue-img-mapper)
[](https://www.npmjs.com/package/vue-img-mapper)
[](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
================================================
Hello
================================================
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
================================================
Hello World {{ counter }}
{{ type }}
Click
Hello
================================================
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"
}
]
}