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
================================================
<!-- DO NOT IGNORE THE TEMPLATE!
Thank you for contributing!
Please fill out all fields below and make sure each item is true and [x] checked.
Otherwise we may not be able to review your PR.
-->
## 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
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
### Linked Issues
<!-- Please insert linked issues -->
### Additional context
<!-- e.g. is there anything you'd like reviewers to focus on? -->
================================================
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
================================================
<p align="center">
<img src="https://img-mapper.nishargshah.dev/logo.png" width="200" style="border-radius: 40px;">
</p>
<h1 align="center">Img Mapper</h1>
<p align="center">
<a style="text-decoration:none" href="https://img-mapper.nishargshah.dev">Documentation</a> |
<a href="https://img-mapper-examples.nishargshah.dev">Examples</a> |
<a href="https://img-mapper.nishargshah.dev/contribute/guide">Contributing</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/react-img-mapper"><img src="https://img.shields.io/npm/v/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1" alt="npm version"></a>
<a href="https://www.npmjs.com/package/react-img-mapper"><img src="https://img.shields.io/npm/dm/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1" alt="npm downloads"></a>
<a href="https://www.npmjs.com/package/react-img-mapper"><img src="https://img.shields.io/npm/last-update/react-img-mapper?style=flat&labelColor=ffffff&color=00acc1" alt="npm last updated"></a>
</p>
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<string, string>)),
'@': 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) => (
<Fragment>
<Analytics />
<Story />
</Fragment>
),
],
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 (
<SyntaxHighlighter showLineNumbers language="javascript" style={atomOneDark}>
{reactCode}
</SyntaxHighlighter>
);
};
addons.register('my/react-code-addon', () => {
addons.add('react-code-addon/panel', {
title: 'React',
type: types.PANEL,
render: ({ active }) => (
<AddonPanel active={active ?? false}>
<ReactContent />
</AddonPanel>
),
});
});
================================================
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 (
<SyntaxHighlighter showLineNumbers language="javascript" style={atomOneDark}>
{vueCode}
</SyntaxHighlighter>
);
};
addons.register('my/vue-code-addon', () => {
addons.add('vue-code-addon/panel', {
title: 'Vue',
type: types.PANEL,
render: ({ active }) => (
<AddonPanel active={active ?? false}>
<VueContent />
</AddonPanel>
),
});
});
================================================
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 <nishargshah3101@gmail.com>",
"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(`(
<ImageMapper
src={url}
name={name}
areas={areas}
active={props.active} // dynamic active
/>
)`);
export const inArrayShowHighlightedAreaCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
/>
)`);
export const disabledAreaCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
disabled={props.disabled} // dynamic disabled
/>
)`);
export const inArrayDisabledAreaCode = inArrayShowHighlightedAreaCode;
export const staySelectedHighlightedAreaCode = mapperWithState(`(
<ImageMapper
src={url}
name={name}
areas={areas}
onChange={(_, newAreas) => setAreas(newAreas)}
isMulti={false}
/>
)`);
export const stayMultipleSelectedHighlightedAreaCode = mapperWithState(`(
<ImageMapper
src={url}
name={name}
areas={areas}
onChange={(_, newAreas) => setAreas(newAreas)}
isMulti
/>
)`);
export const toggleStayHighlightedAreaCode = mapperWithState(`(
<ImageMapper
src={url}
name={name}
areas={areas}
onChange={(_, newAreas) => 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(`(
<ImageMapper
src={url}
name={name}
areas={areas}
/>
)`);
export const inArrayFillColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
/>
)`);
export const dynamicFillColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
fillColor={props.fillColor} // dynamic fill color
/>
)`);
export const dynamicMixArrayFillColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
fillColor={props.fillColor} // dynamic fill color
/>
)`);
export const strokeColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
lineWidth={2}
/>
)`);
export const inArrayStrokeColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
lineWidth={2}
/>
)`);
export const dynamicStrokeColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
strokeColor={props.strokeColor} // dynamic stroke color
lineWidth={props.lineWidth} // dynamic stroke line width
/>
)`);
export const dynamicMixArrayStrokeColorCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
strokeColor={props.strokeColor} // dynamic stroke color
lineWidth={props.lineWidth} // dynamic stroke line width
/>
)`);
================================================
FILE: apps/examples/src/code/dynamic.ts
================================================
import mapper from '@/functions/mapper';
const dynamicAllPropertiesCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
isMulti={props.isMulti}
toggle={props.toggle}
active={props.active}
disabled={props.disabled}
fillColor={props.fillColor}
strokeColor={props.strokeColor}
lineWidth={props.lineWidth}
imgWidth={props.imgWidth}
width={props.width}
height={props.height}
natural={props.natural}
responsive={props.responsive}
parentWidth={props.parentWidth}
/>
)`);
export default dynamicAllPropertiesCode;
================================================
FILE: apps/examples/src/code/map.ts
================================================
import mapper from '@/functions/mapper';
export const nonResponsiveDimensionsCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
width={props.width} // dynamic width
height={props.height} // dynamic height
imageWidth={props.imageWidth} // dynamic imageWidth
natural={props.natural} // dynamic natural
/>
)`);
export const responsiveDimensionsCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
responsive
parentWidth={props.parentWidth} // dynamic parentWidth
/>
)`);
export const allDimensionsCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
width={props.width} // dynamic width
height={props.height} // dynamic height
imgWidth={props.imgWidth} // dynamic imgWidth
natural={props.natural} // dynamic natural
responsive={props.responsive} // dynamic responsive
parentWidth={props.parentWidth} // dynamic parentWidth
/>
)`);
================================================
FILE: apps/examples/src/code/simple.ts
================================================
import mapper from '@/functions/mapper';
const simpleCode = mapper(`(
<ImageMapper
src={url}
name={name}
areas={areas}
/>
)`);
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<ImageMapperProps, 'src' | 'name' | 'areas' | 'onChange'>;
const DynamicMapper: Component<DynamicMapperProps> = (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 (
<Fragment>
{TopComponent(
'Dynamic All Properties Example',
<p>
In this example, all the <span className="tag">functionalities</span> developed so far
have been merged into a single demo.
<br />
<br />
<span className="block">Feel free to explore and have fun experimenting!</span>
</p>,
)}
<ImageMapper
{...props}
areas={areas}
name={name}
onChange={(_, newAreas) => setAreas(newAreas)}
src={url}
/>
</Fragment>
);
};
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<ImageMapperProps, 'src' | 'name' | 'areas' | 'onChange'> & {
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<MapperProps> = (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 (
<Fragment>
{TopComponent ? <TopComponent resetAreas={resetAreas} /> : null}
<ImageMapper
{...restProps}
areas={areas}
name={name}
onChange={(_, newAreas) => (isOnChangeNeeded ? setAreas(newAreas) : null)}
src={url}
/>
{BottomComponent ? <BottomComponent resetAreas={resetAreas} /> : null}
</Fragment>
);
};
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) => (
<div className="top_container">
<h1 className="title">{title}</h1>
<div className="top_content">{content}</div>
</div>
);
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<ImageMapperProps, 'parentWidth'>;
const ZoomInZoomOutAreaComp: Component<ZoomInZoomOutAreaCompProps> = (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 (
<Mapper
responsive
parentWidth={zoom}
TopComponent={() =>
TopComponent(
'Zoom In & Zoom Out Area Example',
<p>
In this example, zoom is controlled via the <span className="tag">parentWidth</span>,
which you can adjust using the Storybook <span className="tag">Controls</span> tab.
Click the buttons below to see the <span className="tag">live</span> zoom effect in the
image mapper:
<br />
<br />
<button onClick={() => handleZoom('in')} style={{ marginRight: 8 }} type="button">
Zoom In
</button>
<button onClick={() => handleZoom('out')} type="button">
Zoom Out
</button>
</p>,
)
}
/>
);
};
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<AreasHookOutput['areas']>([]);
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<typeof Mapper>;
type Story = StoryObj<typeof meta>;
export const ShowHighlightedArea: Story = {
render: (args) => {
const { active } = args;
return (
<Mapper
active={active}
TopComponent={() =>
TopComponent(
'Show Highlighted Area Example',
<p>
In this example, you can use the Storybook <span className="tag">Controls</span> tab
to dynamically choose whether to <span className="tag">show or hide</span> the
highlighted areas using the
<span className="tag">active</span> toggle button, based on your preference.
</p>,
)
}
/>
);
},
parameters: {
reactCode: showHighlightedAreaCode,
},
args: {
active: true,
},
argTypes: {
active: {
control: 'boolean',
},
},
};
export const InArrayShowHighlightedArea: Story = {
render: () => (
<Mapper
customJSON={2}
customType="active"
TopComponent={() =>
TopComponent(
'Show Highlighted Area from Area JSON Example',
<p>
This example demonstrates how to selectively <span className="tag">show or hide</span>
<span className="tag">active</span> areas of an image. Here, the{' '}
<span className="tag">window</span>
and <span className="tag">refrigerator</span> <span className="tag">areas</span> are
excluded from visibility.
<br />
<br />
<span className="block note">
<strong>Note:</strong> By default, the <span className="tag">active</span> property is
set to
<span className="tag">true</span> for the remaining areas.
</span>
</p>,
)
}
/>
),
parameters: {
reactCode: inArrayShowHighlightedAreaCode,
},
};
export const DisabledArea: Story = {
render: (args) => {
const { disabled } = args;
return (
<Mapper
disabled={disabled}
TopComponent={() =>
TopComponent(
'Disabled Area Example',
<p>
In this example, you can use the Storybook <span className="tag">Controls</span> tab
to dynamically
<span className="tag">enable or disable</span> event listeners and highlighted areas
using the
<span className="tag">disabled</span> toggle button, according to your preference.
</p>,
)
}
/>
);
},
parameters: {
reactCode: disabledAreaCode,
},
args: {
disabled: true,
},
argTypes: {
disabled: {
control: 'boolean',
},
},
};
export const InArrayDisabledArea: Story = {
render: () => (
<Mapper
customJSON={2}
customType="disabled"
TopComponent={() =>
TopComponent(
'Disabled Area from Area JSON Example',
<p>
This example demonstrates how to selectively{' '}
<span className="tag">enable or disable</span>
specific areas of an image. Here, the <span className="tag">window</span> and
<span className="tag">refrigerator</span> <span className="tag">areas</span> are
excluded from interaction.
<br />
<br />
<span className="block note">
<strong>Note:</strong> By default, the <span className="tag">disabled</span> property
is set to
<span className="tag">false</span> for the remaining areas.
</span>
</p>,
)
}
/>
),
parameters: {
reactCode: inArrayDisabledAreaCode,
},
};
export const StaySelectedHighlightedArea: Story = {
render: () => (
<Mapper
isOnChangeNeeded
isMulti={false}
TopComponent={() =>
TopComponent(
'Stay Selected Highlighted Area Example',
<p>
In this example, you can <span className="tag">freeze</span> specific{' '}
<span className="tag">areas</span>
to keep them <span className="tag">highlighted</span> by clicking, while still being
able to highlight the <span className="tag">remaining</span> areas on hover.
</p>,
)
}
/>
),
parameters: {
reactCode: staySelectedHighlightedAreaCode,
},
};
export const StayMultipleSelectedHighlightedArea: Story = {
render: () => (
<Mapper
isMulti
isOnChangeNeeded
TopComponent={() =>
TopComponent(
'Stay Multiple Selected Highlighted Area Example',
<p>
This example is similar to the{' '}
<span className="tag">Stay Selected Highlighted Area</span> section, with the added
feature of allowing you to freeze <span className="tag">multiple</span> highlighted
areas simultaneously.
</p>,
)
}
/>
),
parameters: {
reactCode: stayMultipleSelectedHighlightedAreaCode,
},
};
export const ClearSelectedHighlightedArea: Story = {
render: () => (
<Mapper
isMulti
isOnChangeNeeded
TopComponent={({ resetAreas }) =>
TopComponent(
'Clear Selected Highlighted Area Example',
<p>
You can clear the <span className="tag">single or multiple</span> selected highlighted
areas by resetting the state to its initial value. Click the button below to see the
changes
<span className="tag">live</span> in the image mapper:
<br />
<br />
<button onClick={resetAreas} type="button">
Clear
</button>
</p>,
)
}
/>
),
parameters: {
reactCode: clearSelectedHighlightedAreaCode,
},
};
export const ToggleStayHighlightedArea: Story = {
render: (args) => {
const { isMulti, toggle } = args;
return (
<Mapper
isOnChangeNeeded
isMulti={isMulti}
toggle={toggle}
TopComponent={() =>
TopComponent(
'Toggle Stay Highlighted Area Example',
<p>
This example introduces the <span className="tag">toggle</span> property, which allows
you to
<span className="tag">toggle</span> previously frozen highlighted areas on and off.
<br />
</p>,
)
}
/>
);
},
parameters: {
reactCode: toggleStayHighlightedAreaCode,
},
args: {
isMulti: true,
toggle: true,
},
argTypes: {
isMulti: {
control: 'boolean',
},
toggle: {
control: 'boolean',
},
},
};
export const ZoomInZoomOutArea: Story = {
render: ({ parentWidth }) => <ZoomInZoomOutAreaComp parentWidth={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<typeof Mapper>;
type Story = StoryObj<typeof meta>;
export const FillColor: Story = {
render: () => (
<Mapper
customJSON={0}
customType="fill"
TopComponent={() =>
TopComponent(
'Fill Color Example',
<p>
In this example, the <span className="tag">fillColor</span> property is not defined in
the
<span className="tag">areas</span> JSON. As a result, the mapper uses its default
<span className="tag">fillColor</span> behavior.
</p>,
)
}
/>
),
parameters: {
reactCode: fillColorCode,
},
};
export const InArrayFillColor: Story = {
render: () => (
<Mapper
customType="fill"
TopComponent={() =>
TopComponent(
'Fill Color from Area JSON Example',
<p>
In this example, the <span className="tag">fillColor</span> property is defined in the
<span className="tag">areas</span> JSON. Therefore, the mapper applies the
<span className="tag">fillColor</span> values from the JSON, resulting in different
<span className="tag">fillColor</span> for each <span className="tag">area</span>.
</p>,
)
}
/>
),
parameters: {
reactCode: inArrayFillColorCode,
},
};
export const DynamicFillColor: Story = {
render: (args) => {
const { fillColor } = args;
return (
<Mapper
customJSON={0}
customType="fill"
fillColor={fillColor}
TopComponent={() =>
TopComponent(
'Dynamic Fill Color Example',
<p>
In this example, you can use the Storybook <span className="tag">Controls</span> tab
to dynamically modify the <span className="tag">fillColor</span> property according to
your preference.
<br />
<br />
<span className="block note">
<strong>Note:</strong> For better visual results, try reducing the opacity of the
<span className="tag">fillColor</span>.
</span>
</p>,
)
}
/>
);
},
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 (
<Mapper
customJSON={1}
customType="fill"
fillColor={fillColor}
TopComponent={() =>
TopComponent(
'Dynamic Mix Array Fill Color Example',
<p>
In this example, we demonstrate how to <span className="tag">exclude</span> a specific
area of an image from the <span className="tag">whole</span> mapping. Here, the{' '}
<span className="tag">wall area</span>
is excluded, any changes made to the <span className="tag">fillColor</span> property
from the
<span className="tag">Controls</span> tab will only apply to the{' '}
<span className="tag">wall area</span>.
<br />
<br />
<span className="block note">
<strong>Note:</strong> The <span className="tag">fillColor</span> property for the
remaining areas is already defined in the JSON data.
</span>
</p>,
)
}
/>
);
},
parameters: {
reactCode: dynamicMixArrayFillColorCode,
},
args: {
fillColor: 'rgba(255, 255, 255, 0.5)',
},
argTypes: {
fillColor: {
control: 'color',
},
},
};
export const StrokeColor: Story = {
render: () => (
<Mapper
customJSON={0}
customType="stroke"
lineWidth={2}
TopComponent={() =>
TopComponent(
'Stroke Color Example',
<p>
In this example, the <span className="tag">strokeColor</span> property is not defined in
the
<span className="tag">areas</span> JSON. Therefore, the mapper applies its default
<span className="tag">strokeColor</span> behavior.
</p>,
)
}
/>
),
parameters: {
reactCode: strokeColorCode,
},
};
export const InArrayStrokeColor: Story = {
render: () => (
<Mapper
customType="stroke"
lineWidth={2}
TopComponent={() =>
TopComponent(
'Stroke Color from Area JSON Example',
<p>
In this example, the <span className="tag">strokeColor</span> property is defined in the
<span className="tag">areas</span> JSON. Hence, the mapper applies the
<span className="tag">strokeColor</span> values directly from the JSON.
</p>,
)
}
/>
),
parameters: {
reactCode: inArrayStrokeColorCode,
},
};
export const DynamicStrokeColor: Story = {
render: (args) => {
const { strokeColor, lineWidth } = args;
return (
<Mapper
customJSON={0}
customType="stroke"
lineWidth={lineWidth}
strokeColor={strokeColor}
TopComponent={() =>
TopComponent(
'Dynamic Stroke Color Example',
<p>
In this example, you can use the Storybook <span className="tag">Controls</span> tab
to dynamically adjust the <span className="tag">strokeColor</span> and{' '}
<span className="tag">lineWidth</span> properties according to your preference.
</p>,
)
}
/>
);
},
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 (
<Mapper
customJSON={1}
customType="stroke"
lineWidth={lineWidth}
strokeColor={strokeColor}
TopComponent={() =>
TopComponent(
'Dynamic Mix Array Stroke Color Example',
<p>
This example demonstrates how to <span className="tag">exclude</span> a specific area
of an image from the <span className="tag">whole</span> mapping. Here, the{' '}
<span className="tag">wall area</span>
is excluded, so any changes to the <span className="tag">strokeColor</span> property
from the
<span className="tag">Controls</span> tab will only apply to the{' '}
<span className="tag">wall area</span>. Changes to the{' '}
<span className="tag">lineWidth</span> property, however, will be applied to all
areas.
<br />
<br />
<span className="block note">
<strong>Note:</strong> The <span className="tag">strokeColor</span> for the
remaining areas is already defined in the JSON data.
</span>
</p>,
)
}
/>
);
},
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<typeof DynamicMapper>;
type Story = StoryObj<typeof meta>;
export const DynamicAllProperties: Story = {
render: (args) => <DynamicMapper {...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<typeof Mapper>;
type Story = StoryObj<typeof meta>;
export const NonResponsiveDimensions: Story = {
render: (args) => {
const { width, height, imgWidth, natural } = args;
return (
<Mapper
height={height}
imgWidth={imgWidth}
natural={natural}
width={width}
TopComponent={() =>
TopComponent(
'Non Responsive Dimensions Example',
<p>
In this example, the <span className="tag">width</span>,{' '}
<span className="tag">height</span>,<span className="tag">imgWidth</span>, and{' '}
<span className="tag">natural</span> properties are available in the Storybook{' '}
<span className="tag">Controls</span> tab. You can adjust them to see the{' '}
<span className="tag">live</span> results in the image mapper.
<br />
<br />
Experimenting with different values in these fields highlights that making the image
mapper fully <span className="tag">responsive</span> can be challenging.
<br />
<br />
<span className="block note">
Note: Full descriptions and explanations of all properties are available in the
GitHub repository.
</span>
</p>,
)
}
/>
);
},
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 (
<Mapper
responsive
parentWidth={parentWidth}
TopComponent={() =>
TopComponent(
'Responsive Dimensions Example',
<p>
In this example, the <span className="tag">responsive</span> and{' '}
<span className="tag">parentWidth</span>
properties are available in the Storybook <span className="tag">Controls</span> tab.
You can adjust them to see the <span className="tag">live</span> results in the image
mapper.
<br />
<br />
By experimenting with different values for <span className="tag">parentWidth</span>,
you'll notice that the mapper becomes responsive. Try copying the code and see
the results, kudos!
<br />
<br />
<span className="block note">
Note: Full descriptions and explanations of all properties are available in the
GitHub repository.
</span>
</p>,
)
}
/>
);
},
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 (
<Mapper
height={height}
imgWidth={imgWidth}
natural={natural}
parentWidth={parentWidth}
responsive={responsive}
width={width}
TopComponent={() =>
TopComponent(
'All Dimensions Example',
<p>
In this example, the <span className="tag">width</span>,{' '}
<span className="tag">height</span>,<span className="tag">imgWidth</span>,{' '}
<span className="tag">natural</span>,<span className="tag">responsive</span>, and{' '}
<span className="tag">parentWidth</span> fields are available in the Storybook{' '}
<span className="tag">Controls</span> tab. You can modify them to see the{' '}
<span className="tag">live</span> results in the image mapper.
<br />
<br />
This example combines all responsive and non-responsive properties, have fun
experimenting!
<br />
<br />
<span className="block note">
Note: Full descriptions and explanations of all properties are available in the
GitHub repository.
</span>
</p>,
)
}
/>
);
},
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<typeof Mapper>;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
render: () => (
<Mapper
TopComponent={() =>
TopComponent(
'Simple Example',
<p>Basic example showcasing the default setup and essential required properties.</p>,
)
}
/>
),
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 (
<Fragment>
<ImageMapper
src={url}
name={name}
areas={areas}
onChange={(_, newAreas) => setAreas(newAreas)}
isMulti
/>
<button onClick={() => setAreas(initialAreas)}>Clear</button>
</Fragment>
)
}
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 (
<Fragment>
<ImageMapper
src={url}
name={name}
areas={areas}
responsive
parentWidth={zoom}
/>
<button style={{ marginRight: 8 }} onClick={() => handleZoom('in')}>Zoom In</button>
<button onClick={() => handleZoom('out')}>Zoom Out</button>
</Fragment>
)
}
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<E = unknown> = FC<E>;
export type Layout<E = unknown> = Component<Children & E>;
================================================
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 <br/> 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 <nishargshah3101@gmail.com>",
"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 <ImageMapper src={url} name={name} areas={areas} />;
};
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 `<div>` | `null` |
| `imgProps` | object | Props for the `<img>` element | `null` |
| `canvasProps` | object | Props for the `<canvas>` element | `null` |
| `mapProps` | object | Props for the `<map>` element | `null` |
| `areaProps` | object \| array | Props for `<area>` 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: <ul><li>**rect**: `top-left-X, top-left-Y, bottom-right-X, bottom-right-Y`</li><li>**circle**: `center-X, center-Y, radius`</li><li>**poly**: List of points defining the polygon as `point-X, point-Y, ...`</li></ul> | **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
<template>
<ImageMapper :src="src" :map="map" />
</template>
<script>
import ImageMapper from 'vue-img-mapper';
export default {
name: 'Mapper',
components: { ImageMapper },
computed: {
src: () => 'https://img-mapper-examples.nishargshah.dev/assets/example.jpg',
map() {
return {
name: 'my-map',
// Get JSON from below URL as an example and put it here
areas: 'https://img-mapper-examples.nishargshah.dev/assets/areas.json',
};
},
},
};
</script>
```
================================================
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 `<div>` | `null` |
| `imgProps` | object | Props for the `<img>` element | `null` |
| `canvasProps` | object | Props for the `<canvas>` element | `null` |
| `mapProps` | object | Props for the `<map>` element | `null` |
| `areaProps` | object \| array | Props for `<area>` 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: <ul><li>**rect**: `top-left-X, top-left-Y, bottom-right-X, bottom-right-Y`</li><li>**circle**: `center-X, center-Y, radius`</li><li>**poly**: List of points defining the polygon as `point-X, point-Y, ...`</li></ul> | **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 <nishargshah3101@gmail.com>",
"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 <nishargshah3101@gmail.com>",
"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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
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<MapArea[]>(initialAreas);
const [parentWidth, setParentWidth] = useState<number>(640);
const [responsive, setResponsive] = useState(false);
const ref = useRef<RefProperties>(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 (
<React.Fragment>
<ImageMapper
ref={ref}
areas={areas}
name={name}
onLoad={(...arg) => console.log('onLoad =>>>>>>>>>>>>', arg)}
parentWidth={responsive ? parentWidth : 0}
responsive={responsive}
src={url}
onChange={(selectedArea, allAreas) => {
console.log(selectedArea, allAreas);
setAreas(allAreas);
}}
/>
<input
max={1000}
min={100}
onChange={(e) => setParentWidth(e.target.valueAsNumber)}
step={40}
type="range"
value={parentWidth}
/>
<button onClick={handleClick} type="button">
Highlight
</button>
<button onClick={() => setAreas(initialAreas)} type="button">
Clear
</button>
<button onClick={() => setResponsive((prev) => !prev)} type="button">
{responsive ? 'Enabled: Responsive' : 'Enable: Responsive'}
</button>
<button onClick={() => console.log(ref.current?.getRefs())} type="button">
Get Ref
</button>
</React.Fragment>
);
};
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<AreasHookOutput['areas']>([]);
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(<ReactPlayground />);
================================================
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<Required<ImageMapperProps>, 'responsive' | 'parentWidth' | 'imgWidth'>;
export type ScaleCoords = (
coords: MapArea['coords'],
scaleCoordsParams: ScaleCoordsParams,
) => number[];
export type ComputeCenter = (
shape: MapArea['shape'],
scaleCoordsParams: ReturnType<ScaleCoords>,
) => Area['center'];
type GetExtendedAreaParams = Pick<
Required<ImageMapperProps>,
'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<ImageMapperProps, RequiredProps>;
================================================
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<Refs['imgRef']>;
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<Required<ImageMapperProps>, 'responsive' | 'parentWidth' | 'natural'>;
export type GetDimensionValues = (
type: 'width' | 'height',
params: GetDimensionValuesParams,
) => number;
type GetDimensionsParams = Omit<GetDimensionValuesParams, 'type'>;
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<E = CanvasRenderingContext2D> = RefObject<CanvasRenderingContext2D | E>;
type DrawArea = 'scaledCoords' | 'fillColor' | 'lineWidth' | 'strokeColor';
export type DrawChosenShape = (area: Pick<Area, DrawArea>, ctx: CTX) => boolean;
export type DrawShape = (area: Pick<Area, 'shape' | DrawArea>, ctx: CTX<null>) => 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<T extends keyof ImageMapperProps> = Pick<ImageMapperProps, T> & {
cb?: (area: MapArea) => void;
};
export type EventListener<T extends keyof ImageMapperProps, E = AreaEvent> = (
params: EventListenerParam,
props: EventListenerProps<T>,
) => (event: E) => void;
export type ImageEventListener<T extends keyof ImageMapperProps> = (
props: EventListenerProps<T>,
) => (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<T extends MapArea = MapArea, R extends keyof T = RequiredMapArea> = Omit<T, R> &
Pick<NoUndefinedField<T>, 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<HTMLProps<HTMLDivElement>, 'ref' | 'id'> | null;
export type ImgProps = Omit<
HTMLProps<HTMLImageElement>,
'ref' | 'src' | 'useMap' | 'onClick' | 'onMouseMove'
> | null;
export type CanvasProps = Omit<HTMLProps<HTMLCanvasElement>, 'ref'> | null;
export type MapProps = Omit<HTMLProps<HTMLMapElement>, 'name'> | null;
export type AreaProps = Omit<
HTMLProps<HTMLAreaElement>,
| 'key'
| 'coords'
| 'onMouseEnter'
| 'onMouseLeave'
| 'onMouseMove'
| 'onMouseDown'
| 'onMouseUp'
| 'onTouchStart'
| 'onTouchEnd'
| 'onClick'
> | null;
export type TouchEvent = ReactTouchEvent<HTMLAreaElement>;
export type AreaEvent = MouseEvent<HTMLAreaElement>;
export type ImageEvent = MouseEvent<HTMLImageElement>;
export type ChangeEventHandler = ((selectedArea: MapArea, areas: MapArea[]) => void) | null;
export type ImageEventHandler = ((event: ImageEvent) => void) | null;
export type EventHandler<T = AreaEvent> = ((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<MapArea, string>;
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<TouchEvent>;
onTouchEnd?: EventHandler<TouchEvent>;
onMouseMove?: EventHandler;
onMouseEnter?: EventHandler;
onMouseLeave?: EventHandler;
onLoad?: LoadEventHandler;
}
export interface ImageMapperPropsWithRef extends ImageMapperProps {
ref?: Ref<RefProperties>;
}
================================================
FILE: packages/react-img-mapper/src/@types/lib.d.ts
================================================
export type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
export type ConditionalKeys<Base, Condition> = 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<ImageMapperPropsWithRef> = ({ 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<boolean>(false);
const areasRef = useRef<MapArea[]>(areas);
const containerRef = useRef<Refs['containerRef']>(null);
const img = useRef<Refs['imgRef']>(null);
const canvas = useRef<Refs['canvasRef']>(null);
const ctx = useRef<CTX<null>['current']>(null);
const interval = useRef<number>(0);
const prevState = useRef<PrevStateRef>({
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 (
<area
alt="map"
{...currentAreaProps}
key={area[areaKeyName] ?? index.toString()}
coords={scaledCoords.join(',')}
href={href ?? currentAreaProps?.href}
onClick={click({ area, index }, { onClick, cb: handleClick })}
onMouseDown={mouseDown({ area, index }, { onMouseDown })}
onMouseEnter={mouseEnter({ area, index }, { onMouseEnter, cb: handleMouseEnter })}
onMouseLeave={mouseLeave({ area, index }, { onMouseLeave, cb: handleMouseLeave })}
onMouseMove={mouseMove({ area, index }, { onMouseMove })}
onMouseUp={mouseUp({ area, index }, { onMouseUp })}
onTouchEnd={touchEnd({ area, index }, { onTouchEnd })}
onTouchStart={touchStart({ area, index }, { onTouchStart })}
shape={shape ?? currentAreaProps?.shape}
className={[
'img-mapper-area',
...(preFillColor ? ['img-mapper-area-highlighted'] : []),
...(currentAreaProps?.className ? [currentAreaProps.className] : []),
].join(' ')}
/>
);
});
return (
<div
{...containerProps}
ref={containerRef}
id="img-mapper"
style={{ ...containerProps?.style, ...styles.container }}
>
<img
alt="map"
role="presentation"
{...imgProps}
ref={img}
onClick={imageClick({ onImageClick })}
onMouseMove={imageMouseMove({ onImageMouseMove })}
src={src}
useMap={`#${name}`}
className={['img-mapper-img', ...(imgProps?.className ? [imgProps.className] : [])].join(
' ',
)}
style={{
...imgProps?.style,
...styles.img(responsive),
...(isRendered ? null : { display: 'none' }),
}}
/>
<canvas
{...canvasProps}
ref={canvas}
style={{ ...canvasProps?.style, ...styles.canvas }}
className={[
'img-mapper-canvas',
...(canvasProps?.className ? [canvasProps.className] : []),
].join(' ')}
/>
<map
{...mapProps}
name={name}
style={{ ...mapProps?.style, ...styles.map(!!onClick) }}
className={['img-mapper-map', ...(mapProps?.className ? [mapProps.className] : [])].join(
' ',
)}
>
{isRendered && !disabled ? renderAreas() : null}
</map>
</div>
);
};
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 = <T extends ImageMapperProps>(props: T): Required<T> =>
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<T>;
================================================
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 <nishargshah3101@gmail.com>",
"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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
================================================
FILE: packages/vue-img-mapper/playground/src/App.vue
================================================
<script setup lang="ts">
import ImageMapper from '@/ImageMapper.vue';
</script>
<template>
<p>Hello</p>
<ImageMapper />
</template>
================================================
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
================================================
<script setup lang="ts">
import { hello } from '@/helpers/area';
import { ref } from 'vue';
const { type } = defineProps<{
type?: 'primary';
}>();
const counter = ref(0);
const handleClick = () => {
counter.value += 1;
};
</script>
<template>
<p>Hello World {{ counter }}</p>
<p>{{ type }}</p>
<button @click="handleClick">Click</button>
<button @click="hello">Hello</button>
</template>
================================================
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"
}
]
}
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
SYMBOL INDEX (74 symbols across 25 files)
FILE: apps/examples/src/components/DynamicMapper.tsx
type DynamicMapperProps (line 15) | type DynamicMapperProps = Omit<ImageMapperProps, 'src' | 'name' | 'areas...
FILE: apps/examples/src/components/Mapper.tsx
type TopComponentProps (line 13) | interface TopComponentProps {
type BottomComponentProps (line 17) | interface BottomComponentProps {
type MapperProps (line 21) | type MapperProps = Omit<ImageMapperProps, 'src' | 'name' | 'areas' | 'on...
FILE: apps/examples/src/components/TopComponent.tsx
type TopComponentElement (line 3) | type TopComponentElement = (title: string, content: ReactNode) => JSX.El...
FILE: apps/examples/src/components/ZoomInZoomOutAreaComp.tsx
type ZoomInZoomOutAreaCompProps (line 10) | type ZoomInZoomOutAreaCompProps = Pick<ImageMapperProps, 'parentWidth'>;
FILE: apps/examples/src/constants/index.ts
constant CONSTANTS (line 1) | const CONSTANTS = {
FILE: apps/examples/src/functions/mapper.ts
type Mapper (line 3) | type Mapper = (code: string) => string;
FILE: apps/examples/src/functions/mapperWithState.ts
type MapperWithState (line 5) | type MapperWithState = (code: string) => string;
FILE: apps/examples/src/hooks/useAreas.ts
type AreasHookOutput (line 7) | interface AreasHookOutput {
type AreasHook (line 11) | type AreasHook = () => AreasHookOutput;
FILE: apps/examples/src/stories/Area.stories.tsx
type Story (line 24) | type Story = StoryObj<typeof meta>;
FILE: apps/examples/src/stories/Colors.stories.tsx
type Story (line 22) | type Story = StoryObj<typeof meta>;
FILE: apps/examples/src/stories/Dynamic.stories.tsx
type Story (line 12) | type Story = StoryObj<typeof meta>;
FILE: apps/examples/src/stories/Map.stories.tsx
type Story (line 17) | type Story = StoryObj<typeof meta>;
FILE: apps/examples/src/stories/Simple.stories.tsx
type Story (line 13) | type Story = StoryObj<typeof meta>;
FILE: apps/examples/src/types/globals.d.ts
type OverrideMapArea (line 4) | interface OverrideMapArea {
FILE: apps/examples/src/types/index.ts
type Children (line 3) | interface Children {
type Component (line 7) | type Component<E = unknown> = FC<E>;
type Layout (line 9) | type Layout<E = unknown> = Component<Children & E>;
FILE: docs/.vitepress/theme/index.ts
method enhanceApp (line 11) | enhanceApp() {
FILE: packages/react-img-mapper/playground/src/hooks/useAreas.ts
type AreasHookOutput (line 5) | interface AreasHookOutput {
type AreasHook (line 9) | type AreasHook = () => AreasHookOutput;
FILE: packages/react-img-mapper/src/@types/area.d.ts
type ScaleCoordsParams (line 4) | type ScaleCoordsParams = GetPropDimensionParams &
type ScaleCoords (line 7) | type ScaleCoords = (
type ComputeCenter (line 12) | type ComputeCenter = (
type GetExtendedAreaParams (line 17) | type GetExtendedAreaParams = Pick<
type GetExtendedArea (line 22) | type GetExtendedArea = (
FILE: packages/react-img-mapper/src/@types/constants.d.ts
type RequiredProps (line 3) | type RequiredProps = 'src' | 'name' | 'areas';
type ImageMapperDefaultProps (line 5) | type ImageMapperDefaultProps = Omit<ImageMapperProps, RequiredProps>;
FILE: packages/react-img-mapper/src/@types/dimensions.d.ts
type ImgRef (line 5) | type ImgRef = RefObject<Refs['imgRef']>;
type GetDimension (line 7) | type GetDimension = (dimension: Dimension, img: ImgRef) => number;
type GetPropDimensionParams (line 9) | interface GetPropDimensionParams {
type GetPropDimension (line 15) | type GetPropDimension = (params: GetPropDimensionParams) => WidthHeight;
type GetDimensionValuesParams (line 17) | type GetDimensionValuesParams = GetPropDimensionParams &
type GetDimensionValues (line 20) | type GetDimensionValues = (
type GetDimensionsParams (line 25) | type GetDimensionsParams = Omit<GetDimensionValuesParams, 'type'>;
type GetDimensions (line 27) | type GetDimensions = (params: GetDimensionsParams) => WidthHeight;
type PrevStateRef (line 29) | interface PrevStateRef {
FILE: packages/react-img-mapper/src/@types/draw.d.ts
type CTX (line 5) | type CTX<E = CanvasRenderingContext2D> = RefObject<CanvasRenderingContex...
type DrawArea (line 7) | type DrawArea = 'scaledCoords' | 'fillColor' | 'lineWidth' | 'strokeColor';
type DrawChosenShape (line 9) | type DrawChosenShape = (area: Pick<Area, DrawArea>, ctx: CTX) => boolean;
type DrawShape (line 11) | type DrawShape = (area: Pick<Area, 'shape' | DrawArea>, ctx: CTX<null>) ...
type GetShape (line 13) | type GetShape = (shape: Area['shape']) => DrawChosenShape | false;
FILE: packages/react-img-mapper/src/@types/events.d.ts
type EventListenerParam (line 3) | interface EventListenerParam {
type EventListenerProps (line 8) | type EventListenerProps<T extends keyof ImageMapperProps> = Pick<ImageMa...
type EventListener (line 12) | type EventListener<T extends keyof ImageMapperProps, E = AreaEvent> = (
type ImageEventListener (line 17) | type ImageEventListener<T extends keyof ImageMapperProps> = (
FILE: packages/react-img-mapper/src/@types/index.d.ts
type Refs (line 5) | interface Refs {
type RefProperties (line 11) | interface RefProperties {
type OverrideMapArea (line 16) | interface OverrideMapArea {}
type MapArea (line 18) | interface MapArea extends OverrideMapArea {
type RequiredMapArea (line 31) | type RequiredMapArea = 'active' | 'fillColor' | 'lineWidth' | 'strokeCol...
type RequiredArea (line 32) | type RequiredArea<T extends MapArea = MapArea, R extends keyof T = Requi...
type Area (line 35) | interface Area extends RequiredArea {
type WidthHeight (line 40) | interface WidthHeight {
type Dimension (line 45) | type Dimension = number | ((event: HTMLImageElement) => number);
type ContainerProps (line 47) | type ContainerProps = Omit<HTMLProps<HTMLDivElement>, 'ref' | 'id'> | null;
type ImgProps (line 48) | type ImgProps = Omit<
type CanvasProps (line 52) | type CanvasProps = Omit<HTMLProps<HTMLCanvasElement>, 'ref'> | null;
type MapProps (line 53) | type MapProps = Omit<HTMLProps<HTMLMapElement>, 'name'> | null;
type AreaProps (line 54) | type AreaProps = Omit<
type TouchEvent (line 68) | type TouchEvent = ReactTouchEvent<HTMLAreaElement>;
type AreaEvent (line 69) | type AreaEvent = MouseEvent<HTMLAreaElement>;
type ImageEvent (line 70) | type ImageEvent = MouseEvent<HTMLImageElement>;
type ChangeEventHandler (line 72) | type ChangeEventHandler = ((selectedArea: MapArea, areas: MapArea[]) => ...
type ImageEventHandler (line 73) | type ImageEventHandler = ((event: ImageEvent) => void) | null;
type EventHandler (line 74) | type EventHandler<T = AreaEvent> = ((area: MapArea, index: number, e: T)...
type LoadEventHandler (line 75) | type LoadEventHandler = ((event: HTMLImageElement, dimensions: WidthHeig...
type ImageMapperProps (line 77) | interface ImageMapperProps {
type ImageMapperPropsWithRef (line 115) | interface ImageMapperPropsWithRef extends ImageMapperProps {
FILE: packages/react-img-mapper/src/@types/lib.d.ts
type NoUndefinedField (line 1) | type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNulla...
type ConditionalKeys (line 3) | type ConditionalKeys<Base, Condition> = NonNullable<
FILE: packages/react-img-mapper/src/@types/styles.d.ts
type StylesProps (line 3) | interface StylesProps {
Condensed preview — 119 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (183K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 271,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@3.1.1/schema.json\",\n \"changelog\": false,\n \"commit\": false,\n \"fix"
},
{
"path": ".gitattributes",
"chars": 229,
"preview": "# Project files\n.gitattributes text eol=lf\n.gitignore text eol=lf\n.npmrc text eol=lf\n.nvmrc text eol=lf\n.prettierignore "
},
{
"path": ".github/CODEOWNERS",
"chars": 14,
"preview": "* @NishargShah"
},
{
"path": ".github/ISSUE_TEMPLATE/1.bug.yml",
"chars": 1862,
"preview": "name: Bug report 🐛\ndescription: Create a bug report\nassignees:\n - NishargShah\nlabels:\n - new\n - bug\nbody:\n - type: m"
},
{
"path": ".github/ISSUE_TEMPLATE/2.feature.yml",
"chars": 1017,
"preview": "name: Feature request 🚀\ndescription: Suggest an idea for this project\nassignees:\n - NishargShah\nlabels:\n - new\n - enh"
},
{
"path": ".github/pull_request_template.md",
"chars": 757,
"preview": "<!-- DO NOT IGNORE THE TEMPLATE!\n\nThank you for contributing!\n\nPlease fill out all fields below and make sure each item "
},
{
"path": ".github/stale.yml",
"chars": 684,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 14\n# Number of days of inactivity before a "
},
{
"path": ".github/workflows/validate-pr.yml",
"chars": 734,
"preview": "name: Validate Pull Request\npermissions:\n contents: read\n\non:\n pull_request:\n branches:\n - master\n - cana"
},
{
"path": ".gitignore",
"chars": 298,
"preview": "# dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\nbuild\ndist\ncache\n\n# misc\n.DS_Store\n*.pem\n\n# d"
},
{
"path": ".husky/pre-commit",
"chars": 17,
"preview": "pnpm lint-staged\n"
},
{
"path": ".husky/pre-push",
"chars": 38,
"preview": "pnpm --silent script:lint --for=check\n"
},
{
"path": ".npmrc",
"chars": 37,
"preview": "script-shell=bash\nengine-strict=true\n"
},
{
"path": ".nvmrc",
"chars": 3,
"preview": "24\n"
},
{
"path": ".prettierignore",
"chars": 15,
"preview": "pnpm-lock.yaml\n"
},
{
"path": "CHANGELOG.md",
"chars": 4857,
"preview": "## 2.1.0 (2025-10-00)\n\n### 🚨 Breaking Change\n\n- Introduce `vue-img-mapper` package.\n- **react-img-mapper:** ESM is by de"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5220,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "CONTRIBUTING.md",
"chars": 98,
"preview": "# Contributing\n\nPlease refer [Contributing](https://img-mapper.nishargshah.dev/contribute/guide).\n"
},
{
"path": "LICENSE.txt",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2025 Nisharg Shah\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 1434,
"preview": "<p align=\"center\">\n <img src=\"https://img-mapper.nishargshah.dev/logo.png\" width=\"200\" style=\"border-radius: 40px;\">\n</"
},
{
"path": "SECURITY.md",
"chars": 720,
"preview": "# Security Policy\n\nIf you discover a security vulnerability in this project, please report it responsibly:\n\n1. **Do not*"
},
{
"path": "apps/examples/.storybook/main.ts",
"chars": 876,
"preview": "import path from 'node:path';\n\nimport type { StorybookConfig } from '@storybook/react-vite';\n\nconst config = {\n stories"
},
{
"path": "apps/examples/.storybook/preview.tsx",
"chars": 759,
"preview": "import { Fragment } from 'react';\n\nimport { Analytics } from '@vercel/analytics/react';\n\nimport '@/styles/stories.css';\n"
},
{
"path": "apps/examples/.storybook/react-code-addon/register.tsx",
"chars": 892,
"preview": "/* eslint-disable import-x/no-extraneous-dependencies */\n// DON'T REMOVE REACT FROM HERE\nimport React from 'react';\n\nimp"
},
{
"path": "apps/examples/.storybook/vue-code-addon/register.tsx",
"chars": 876,
"preview": "/* eslint-disable import-x/no-extraneous-dependencies */\n// DON'T REMOVE REACT FROM HERE\nimport React from 'react';\n\nimp"
},
{
"path": "apps/examples/package.json",
"chars": 1132,
"preview": "{\n \"name\": \"examples\",\n \"version\": \"2.0.3\",\n \"private\": true,\n \"description\": \"Examples of react-img-mapper and vue-"
},
{
"path": "apps/examples/public/assets/areas.json",
"chars": 18370,
"preview": "[\n {\n \"id\": \"469f9800-c45a-483f-b13e-bd24f3fb79f4\",\n \"title\": \"Hardwood\",\n \"shape\": \"poly\",\n \"name\": \"1\",\n "
},
{
"path": "apps/examples/src/code/areas.ts",
"chars": 1608,
"preview": "import mapper from '@/functions/mapper';\nimport mapperWithState from '@/functions/mapperWithState';\n\nexport const showHi"
},
{
"path": "apps/examples/src/code/colors.ts",
"chars": 1519,
"preview": "import mapper from '@/functions/mapper';\n\nexport const fillColorCode = mapper(`(\n <ImageMapper \n src={url} \n "
},
{
"path": "apps/examples/src/code/dynamic.ts",
"chars": 627,
"preview": "import mapper from '@/functions/mapper';\n\nconst dynamicAllPropertiesCode = mapper(`(\n <ImageMapper \n src={url} \n"
},
{
"path": "apps/examples/src/code/map.ts",
"chars": 1031,
"preview": "import mapper from '@/functions/mapper';\n\nexport const nonResponsiveDimensionsCode = mapper(`(\n <ImageMapper \n s"
},
{
"path": "apps/examples/src/code/simple.ts",
"chars": 186,
"preview": "import mapper from '@/functions/mapper';\n\nconst simpleCode = mapper(`(\n <ImageMapper \n src={url} \n name={na"
},
{
"path": "apps/examples/src/components/DynamicMapper.tsx",
"chars": 1676,
"preview": "import { Fragment, useEffect, useState } from 'react';\n\nimport ImageMapper from 'react-img-mapper';\n\nimport TopComponent"
},
{
"path": "apps/examples/src/components/Mapper.tsx",
"chars": 3019,
"preview": "import { Fragment, useCallback, useEffect, useState } from 'react';\n\nimport ImageMapper from 'react-img-mapper';\n\nimport"
},
{
"path": "apps/examples/src/components/TopComponent.tsx",
"chars": 354,
"preview": "import type { JSX, ReactNode } from 'react';\n\ntype TopComponentElement = (title: string, content: ReactNode) => JSX.Elem"
},
{
"path": "apps/examples/src/components/ZoomInZoomOutAreaComp.tsx",
"chars": 1581,
"preview": "import { useState } from 'react';\n\nimport Mapper from '@/components/Mapper';\nimport TopComponent from '@/components/TopC"
},
{
"path": "apps/examples/src/constants/index.ts",
"chars": 218,
"preview": "const CONSTANTS = {\n url: 'https://img-mapper-examples.nishargshah.dev/assets/example.jpg',\n name: 'my-map',\n areasUr"
},
{
"path": "apps/examples/src/functions/mapper.ts",
"chars": 334,
"preview": "import variablesTemplate from '@/templates/variablesTemplate';\n\ntype Mapper = (code: string) => string;\n\nconst mapper: M"
},
{
"path": "apps/examples/src/functions/mapperWithState.ts",
"chars": 559,
"preview": "import CONSTANTS from '@/constants';\n\nconst { url, name, areasUrl } = CONSTANTS;\n\ntype MapperWithState = (code: string) "
},
{
"path": "apps/examples/src/hooks/useAreas.ts",
"chars": 555,
"preview": "import { useEffect, useState } from 'react';\n\nimport CONSTANTS from '@/constants';\n\nimport type { MapArea } from 'react-"
},
{
"path": "apps/examples/src/stories/Area.stories.tsx",
"chars": 7642,
"preview": "import {\n clearSelectedHighlightedAreaCode,\n disabledAreaCode,\n inArrayDisabledAreaCode,\n inArrayShowHighlightedArea"
},
{
"path": "apps/examples/src/stories/Colors.stories.tsx",
"chars": 8052,
"preview": "import {\n dynamicFillColorCode,\n dynamicMixArrayFillColorCode,\n dynamicMixArrayStrokeColorCode,\n dynamicStrokeColorC"
},
{
"path": "apps/examples/src/stories/Dynamic.stories.tsx",
"chars": 1482,
"preview": "import dynamicAllPropertiesCode from '@/code/dynamic';\nimport DynamicMapper from '@/components/DynamicMapper';\n\nimport t"
},
{
"path": "apps/examples/src/stories/Map.stories.tsx",
"chars": 5487,
"preview": "import {\n allDimensionsCode,\n nonResponsiveDimensionsCode,\n responsiveDimensionsCode,\n} from '@/code/map';\nimport Map"
},
{
"path": "apps/examples/src/stories/Simple.stories.tsx",
"chars": 685,
"preview": "import simpleCode from '@/code/simple';\nimport Mapper from '@/components/Mapper';\nimport TopComponent from '@/components"
},
{
"path": "apps/examples/src/styles/stories.css",
"chars": 712,
"preview": ":root {\n --tag: #1b1f230d;\n --block: #efefef;\n --block-border: #cecece;\n --tag-block: #f6f8fa;\n}\n\nbody {\n font-fami"
},
{
"path": "apps/examples/src/templates/clearButtonTemplate.ts",
"chars": 795,
"preview": "import CONSTANTS from '@/constants';\n\nconst { url, name, areasUrl } = CONSTANTS;\n\nconst clearButtonTemplate = `import Re"
},
{
"path": "apps/examples/src/templates/variablesTemplate.ts",
"chars": 284,
"preview": "import CONSTANTS from '@/constants';\n\nconst { url, name, areasUrl } = CONSTANTS;\n\nconst variablesTemplate = `const url ="
},
{
"path": "apps/examples/src/templates/zoomTemplate.ts",
"chars": 907,
"preview": "import variablesTemplate from '@/templates/variablesTemplate';\n\nconst zoomTemplate = `import React, { Fragment, useState"
},
{
"path": "apps/examples/src/types/globals.d.ts",
"chars": 119,
"preview": "import 'react-img-mapper';\n\ndeclare module 'react-img-mapper' {\n interface OverrideMapArea {\n title: string;\n }\n}\n"
},
{
"path": "apps/examples/src/types/index.ts",
"chars": 203,
"preview": "import type { FC, ReactNode } from 'react';\n\nexport interface Children {\n children: ReactNode;\n}\n\nexport type Component"
},
{
"path": "apps/examples/tsconfig.json",
"chars": 397,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"jsx\":"
},
{
"path": "apps/examples/vercel.json",
"chars": 243,
"preview": "{\n \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n \"buildCommand\": \"pnpm build\",\n \"cleanUrls\": true,\n \"devComma"
},
{
"path": "docs/.vitepress/config.mts",
"chars": 5057,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { defineConfig"
},
{
"path": "docs/.vitepress/theme/index.ts",
"chars": 395,
"preview": "import { inject } from '@vercel/analytics';\n// eslint-disable-next-line import-x/no-unresolved\nimport 'virtual:group-ico"
},
{
"path": "docs/.vitepress/theme/style.css",
"chars": 1224,
"preview": "/**\n * Theme\n * -------------------------------------------------------------------------- */\n\n:root {\n --vp-c-brand-1:"
},
{
"path": "docs/contribute/guide.md",
"chars": 2586,
"preview": "# Contributing {#contributing}\n\nThank you for considering contributing to `img-mapper`. We welcome all contributions, wh"
},
{
"path": "docs/guide/examples.md",
"chars": 1033,
"preview": "# Examples {#examples}\n\nExplore live demos of Img Mapper at: [**img-mapper-examples.nishargshah.dev**](https://img-mappe"
},
{
"path": "docs/guide/getting-started.md",
"chars": 1501,
"preview": "# Getting Started {#getting-started}\n\n**Img Mapper** is an open-source library that enables developers to create **inter"
},
{
"path": "docs/index.md",
"chars": 1121,
"preview": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\nhero:\n name: 'Img Mapper'\n tagline: A Reac"
},
{
"path": "docs/package.json",
"chars": 811,
"preview": "{\n \"name\": \"img-mapper-docs\",\n \"version\": \"2.0.3\",\n \"private\": true,\n \"description\": \"Documentation of img-mapper\",\n"
},
{
"path": "docs/react/installation.md",
"chars": 782,
"preview": "# Installation {#installation}\n\nInstall **react-img-mapper** using your preferred package manager:\n\n::: code-group\n\n```s"
},
{
"path": "docs/react/properties.md",
"chars": 9618,
"preview": "# Properties {#properties}\n\nTogether, below sections let you fully control the component, customize its behavior and app"
},
{
"path": "docs/tsconfig.json",
"chars": 349,
"preview": "{\n \"extends\": \"../tsconfig.json\",\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"baseUrl\""
},
{
"path": "docs/vercel.json",
"chars": 242,
"preview": "{\n \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n \"buildCommand\": \"pnpm build\",\n \"cleanUrls\": true,\n \"devComma"
},
{
"path": "docs/vue/installation.md",
"chars": 976,
"preview": "# Installation {#installation}\n\n::: warning\n\n`vue-img-mapper` is currently in beta. Features and APIs are still evolving"
},
{
"path": "docs/vue/properties.md",
"chars": 9729,
"preview": "# Properties {#properties}\n\n::: warning\n\n`vue-img-mapper` is currently in beta. Features and APIs are still evolving and"
},
{
"path": "eslint.config.mjs",
"chars": 669,
"preview": "import customGeneralESLintConfig from './lint/general.eslint.mjs';\nimport customImportESLintConfig from './lint/import.e"
},
{
"path": "lint/general.eslint.mjs",
"chars": 1283,
"preview": "const customGeneralESLintConfig = [\n {\n name: 'x/general/rules',\n rules: {\n 'no-console': 'off',\n 'no-v"
},
{
"path": "lint/import.eslint.mjs",
"chars": 1076,
"preview": "import { rules } from 'eslint-config-airbnb-extended';\nimport unusedImportsPlugin from 'eslint-plugin-unused-imports';\n\n"
},
{
"path": "lint/javascript.eslint.mjs",
"chars": 1130,
"preview": "import js from '@eslint/js';\nimport { configs, plugins } from 'eslint-config-airbnb-extended';\nimport promisePlugin from"
},
{
"path": "lint/prettier.eslint.mjs",
"chars": 474,
"preview": "import { rules as prettierConfigRules } from 'eslint-config-prettier';\nimport prettierPlugin from 'eslint-plugin-prettie"
},
{
"path": "lint/react.eslint.mjs",
"chars": 720,
"preview": "import { configs, plugins, rules } from 'eslint-config-airbnb-extended';\n\nconst customReactESLintConfig = [\n // React P"
},
{
"path": "lint/typescript.eslint.mjs",
"chars": 651,
"preview": "import { configs, plugins, rules } from 'eslint-config-airbnb-extended';\n\nconst customTSESLintConfig = [\n // TypeScript"
},
{
"path": "lint/utils.eslint.mjs",
"chars": 210,
"preview": "import path from 'node:path';\n\nimport { includeIgnoreFile } from '@eslint/compat';\n\nexport const gitignorePath = path.re"
},
{
"path": "lint-staged.config.mjs",
"chars": 119,
"preview": "/**\n * @type {import('lint-staged').Configuration}\n */\nexport default {\n '*.{js,mjs,jsx,ts,mts,tsx}': 'pnpm lint',\n};\n"
},
{
"path": "package.json",
"chars": 2604,
"preview": "{\n \"name\": \"img-mapper-project\",\n \"version\": \"2.0.3\",\n \"description\": \"Creates Interactive and Highlighted Zones on I"
},
{
"path": "packages/react-img-mapper/.npmignore",
"chars": 81,
"preview": "# Ignore everything\n/*\n\n# Not ignored folders\n!dist\n\n# Inside dist\n*.tsbuildinfo\n"
},
{
"path": "packages/react-img-mapper/README.md",
"chars": 667,
"preview": "# `react-img-mapper`\n\n[.mount('#app');\n"
},
{
"path": "packages/vue-img-mapper/src/ImageMapper.vue",
"chars": 404,
"preview": "<script setup lang=\"ts\">\nimport { hello } from '@/helpers/area';\nimport { ref } from 'vue';\n\nconst { type } = defineProp"
},
{
"path": "packages/vue-img-mapper/src/helpers/area.ts",
"chars": 121,
"preview": "// eslint-disable-next-line import-x/prefer-default-export\nexport const hello = (): void => {\n console.log('hello');\n};"
},
{
"path": "packages/vue-img-mapper/src/index.ts",
"chars": 95,
"preview": "// eslint-disable-next-line no-restricted-exports\nexport { default } from '@/ImageMapper.vue';\n"
},
{
"path": "packages/vue-img-mapper/tsconfig.json",
"chars": 330,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"baseU"
},
{
"path": "packages/vue-img-mapper/tsdown.config.mts",
"chars": 389,
"preview": "// eslint-disable-next-line import-x/no-extraneous-dependencies\nimport { defineConfig } from 'tsdown';\n\nexport default d"
},
{
"path": "packages/vue-img-mapper/vite.config.ts",
"chars": 374,
"preview": "import { fileURLToPath } from 'node:url';\n\nimport vue from '@vitejs/plugin-vue';\nimport { defineConfig } from 'vite';\n\ne"
},
{
"path": "pnpm-workspace.yaml",
"chars": 304,
"preview": "packages:\n - docs\n - apps/*\n - packages/*\n\ncatalog:\n '@types/react': ^19.2.2\n '@types/react-dom': ^19.2.2\n '@verce"
},
{
"path": "prettier.config.mjs",
"chars": 196,
"preview": "/**\n * @see https://prettier.io/docs/configuration\n * @type {import(\"prettier\").Config}\n */\nexport default {\n printWidt"
},
{
"path": "scripts/lint.sh",
"chars": 1170,
"preview": "#!/bin/bash\n\nmode=\"fix\"\n\nfilters=()\n\nfor arg in \"$@\"; do\n case $arg in\n --for=*)\n mode=\"${arg#--for=}\"\n ;;\n --f"
},
{
"path": "tsconfig.json",
"chars": 634,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"preserve\",\n \"moduleResolution\": \"bundler\",\n \"lib\":"
}
]
About this extraction
This page contains the full source code of the img-mapper/react-img-mapper GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 119 files (163.9 KB), approximately 47.1k tokens, and a symbol index with 74 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.