#Getting started

#Install

bun add @gravn/muscle-highlighter
# React Native only - the web entry needs nothing extra
bun add react-native-svg

react-native-svg is an optional peer dependency. A web-only consumer never installs it, and never bundles it.

#Render a body

import { MuscleMap } from "@gravn/muscle-highlighter/native";

export function SessionSummary() {
  return (
    <MuscleMap
      sex="male"
      view="front"
      intensities={{ CHE: 9, DEL: 7, TRI: 4 }}
    />
  );
}

That is the whole minimum. Everything else - colours, sizing, press handling - has a default.

#Pick the right entry point

The package splits by import path, not by bundler platform detection:

Import What you get
@gravn/muscle-highlighter Pure data and functions. No React, no renderer. Safe on a server, in a worker, or in a test.
@gravn/muscle-highlighter/native MuscleMap built on react-native-svg.
@gravn/muscle-highlighter/web MuscleMap built on DOM SVG.

Metro honours .native.ts file extensions but most web bundlers do not without extra config, so relying on that would have made web setups fragile. Instead the renderer takes its SVG primitives as an argument and each entry point binds a concrete set. The practical upshot: react-native-svg can never leak into a web bundle, whatever your bundler does.

The root entry deliberately does not export MuscleMap. Importing catalogue helpers on a server should never drag a renderer in with them.

#The package ships TypeScript source

There is no build step and no dist/. Metro transpiles the source as-is. Next.js needs the package listed in transpilePackages:

// next.config.js
module.exports = {
  transpilePackages: ["@gravn/muscle-highlighter"],
};

Vite and most other bundlers need nothing.

#Consuming it from a sibling directory

If the package sits beside your app rather than in node_modules - a monorepo, or a checkout next door - Metro needs help, and the naive approaches fail in ways worth knowing about.

A file: dependency does not work with bun. Bun copies the directory instead of symlinking it, so edits to the package never reach the app, and the package's own node_modules comes along - giving the bundle two copies of react-native-svg, two native module registrations, and SVGs that silently fail to render.

Expo's CLI overrides watchFolders. Setting it in metro.config.js is not enough on its own; Metro reports Failed to get the SHA-1 or claims the files do not exist.

The reliable route is a real workspace - a root package.json with a workspaces field - which Expo detects natively and configures Metro for. Whatever the arrangement, pin the singletons:

// metro.config.js
const SINGLETON_PACKAGES = new Set([
  "react",
  "react-dom",
  "react-native",
  "react-native-svg",
]);

config.resolver.resolveRequest = (context, moduleName, platform) => {
  const packageName = moduleName.startsWith("@")
    ? moduleName.split("/").slice(0, 2).join("/")
    : moduleName.split("/")[0];

  if (SINGLETON_PACKAGES.has(packageName)) {
    // Re-root the lookup at the app, whichever file asked for it.
    return context.resolveRequest(
      { ...context, originModulePath: path.join(projectRoot, "package.json") },
      moduleName,
      platform,
    );
  }
  return context.resolveRequest(context, moduleName, platform);
};

Without that hook, a duplicated react-native-svg produces a body map that renders nothing at all and logs no error - the worst possible failure mode.

#Styling it

Four props carry the whole look:

<MuscleMap
  bodyFill="#232833" // the silhouette, drawn beneath the muscles
  baseColor="#39404f" // muscles with no intensity
  outlineColor="#5b6478" // the silhouette stroke, drawn over the muscles
  muscleStroke="#1b1f27" // optional per-muscle outline, for a segmented look
/>

Draw order is silhouette fill, then muscles, then silhouette stroke - so a muscle fill can never bleed past the body's edge.

Sizing is width/height, or scale as a multiplier over the 200×400 default. The aspect ratio is fixed by the artwork at 1:2.