#Getting started
#Install
bun add @gravn/muscle-highlighter
# React Native only - the web entry needs nothing extra
bun add react-native-svgreact-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.