@jlunamena/design-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Design System Contract
2
+
3
+ Short and practical agreement before building anything.
4
+
5
+ ## 1) Purpose
6
+ - This design system provides reusable presentational components for the Expo app.
7
+ - It owns UI, visual states, tokens, and component consistency.
8
+ - The app owns data, navigation, API calls, and business logic.
9
+
10
+ ## 2) What It Exports
11
+ Tokens:
12
+ - colors, spacing, typography, radius, shadows
13
+
14
+ Theme:
15
+ - ThemeProvider, useTheme
16
+
17
+ Components:
18
+ - Button, Text, Input, Card, Icon, Badge, Avatar, Divider
19
+
20
+ Layout:
21
+ - Box, Stack, Screen, Container
22
+
23
+ ## 3) What It Does NOT Export
24
+ - Screens
25
+ - Navigation
26
+ - API logic
27
+ - Auth logic
28
+ - Workout logic
29
+ - Database logic
30
+ - Business rules
31
+
32
+ ## 4) Component Done Checklist
33
+ - Has variants
34
+ - Has sizes
35
+ - Has states
36
+ - Has Storybook examples
37
+ - Has documented props
38
+ - Works in Expo app
39
+ - Reviewed visually by you
40
+ - Reviewed technically by him
41
+
42
+ ## 5) First Components to Build
43
+ - Button
44
+ - Text
45
+ - Input
46
+ - Card
47
+ - Screen
48
+ - Box / Stack
49
+
50
+ ## 6) Repo Workflow
51
+ Design system repo:
52
+ - Build component
53
+ - Document in Storybook
54
+ - Release/update package
55
+
56
+ Expo app repo:
57
+ - Install/update design system
58
+ - Connect real actions/data
59
+ - Test in app
60
+
61
+ ## 7) Repo Structure Logic
62
+ - [src/tokens](src/tokens) holds design tokens only (no component logic).
63
+ - [src/theme](src/theme) exposes theme utilities and providers.
64
+ - [src/components](src/components) is one folder per component (no app screens).
65
+ - [src/layout](src/layout) holds layout primitives only.
66
+ - [src/stories/foundations](src/stories/foundations) documents tokens and foundations.
67
+ - [src/stories/components](src/stories/components) documents UI components.
68
+ - [src/stories/layout](src/stories/layout) documents layout primitives.
69
+ - [src/stories/examples](src/stories/examples) is for small composed examples.
70
+
71
+ ## 8) Running Storybook
72
+ - On device: `npm run storybook:ios` or `npm run storybook:android`
73
+ - On web: `npm run storybook:web`
74
+
75
+ ## 8.1) Storybook Notes (Current Setup)
76
+ - Stories are loaded from `storybook/storybook.requires.ts` via explicit `require` entries.
77
+ - The Intro story is a simple menu to jump to core stories on device.
78
+ - If you add new stories, update the require list so they show up.
79
+
80
+ ## 9) Running the App (without Storybook)
81
+ - `npm run ios`
82
+ - `npm run android`
83
+ - `npm run web`
84
+
85
+ ## 10) Public Exports (Auto-Generated)
86
+ Exports are generated into `src/index.ts` so new components can be added without
87
+ manually editing a root barrel.
88
+
89
+ How it works:
90
+ - Any folder in `src/components`, `src/layout`, or `src/theme` that has its own
91
+ `index.ts` (or `index.tsx`) will be exported automatically.
92
+ - `src/tokens/index.ts` is always exported.
93
+
94
+ Commands:
95
+ - `npm run generate:exports` regenerates `src/index.ts`.
@@ -0,0 +1,127 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ declare const colors: {
4
+ readonly primary: "#FDC026";
5
+ readonly accent: "#6AABDE";
6
+ readonly bodyDiagram: {
7
+ readonly muscleFill: "#FFBF00";
8
+ readonly muscleStroke: "#FF0090";
9
+ readonly muscleStrokeWidth: 2;
10
+ readonly muscleOpacity: 1;
11
+ };
12
+ readonly background: {
13
+ readonly default: "#161C26";
14
+ readonly muted: "#1C2434";
15
+ readonly highlight: "#383040";
16
+ readonly depth: "#08080A";
17
+ };
18
+ readonly text: {
19
+ readonly primary: "#E8E8E8";
20
+ readonly secondary: "#9B9B9B";
21
+ };
22
+ };
23
+ type Colors = typeof colors;
24
+
25
+ declare const spacing: {
26
+ readonly big: 32;
27
+ readonly default: 24;
28
+ readonly small: 12;
29
+ readonly textHorizontal: 4;
30
+ readonly textVertical: 0;
31
+ };
32
+ type Spacing = typeof spacing;
33
+
34
+ declare const radius: {
35
+ readonly button: 4;
36
+ readonly card: 8;
37
+ readonly input: 4;
38
+ readonly full: 9999;
39
+ };
40
+ type Radius = typeof radius;
41
+
42
+ declare const typography: {
43
+ readonly size: {
44
+ readonly xs: 11;
45
+ readonly sm: 13;
46
+ readonly base: 15;
47
+ readonly md: 17;
48
+ readonly lg: 20;
49
+ readonly xl: 24;
50
+ readonly '2xl': 30;
51
+ readonly '3xl': 36;
52
+ };
53
+ readonly weight: {
54
+ readonly regular: "400";
55
+ readonly medium: "500";
56
+ readonly semibold: "600";
57
+ readonly bold: "700";
58
+ };
59
+ readonly lineHeight: {
60
+ readonly tight: 1.2;
61
+ readonly normal: 1.5;
62
+ readonly relaxed: 1.75;
63
+ };
64
+ };
65
+ type Typography = typeof typography;
66
+
67
+ declare const shadows: {
68
+ readonly sm: {
69
+ readonly shadowColor: "#08080A";
70
+ readonly shadowOffset: {
71
+ readonly width: 0;
72
+ readonly height: 1;
73
+ };
74
+ readonly shadowOpacity: 0.4;
75
+ readonly shadowRadius: 2;
76
+ readonly elevation: 2;
77
+ };
78
+ readonly md: {
79
+ readonly shadowColor: "#08080A";
80
+ readonly shadowOffset: {
81
+ readonly width: 0;
82
+ readonly height: 4;
83
+ };
84
+ readonly shadowOpacity: 0.5;
85
+ readonly shadowRadius: 8;
86
+ readonly elevation: 4;
87
+ };
88
+ readonly lg: {
89
+ readonly shadowColor: "#08080A";
90
+ readonly shadowOffset: {
91
+ readonly width: 0;
92
+ readonly height: 8;
93
+ };
94
+ readonly shadowOpacity: 0.6;
95
+ readonly shadowRadius: 16;
96
+ readonly elevation: 8;
97
+ };
98
+ };
99
+ type Shadows = typeof shadows;
100
+
101
+ type BodyDiagramView = 'front' | 'back';
102
+ type BodyDiagramPalette = {
103
+ muscleFill?: string;
104
+ muscleStroke?: string;
105
+ muscleStrokeWidth?: number;
106
+ muscleOpacity?: number;
107
+ };
108
+ type BodyDiagramOverlayStyle = {
109
+ blur?: number;
110
+ blendMode?: string;
111
+ };
112
+ type BodyDiagramIntensity = 'off' | 'low' | 'medium' | 'high';
113
+ type BodyDiagramProps = {
114
+ view: BodyDiagramView;
115
+ svgMarkupByView: Record<BodyDiagramView, string>;
116
+ baseImageByView?: Partial<Record<BodyDiagramView, string>>;
117
+ selectedMuscles: string[];
118
+ onSelectedChange: (selected: string[]) => void;
119
+ debugForceVisible?: boolean;
120
+ interactionMode?: 'auto' | 'editable' | 'readonly';
121
+ palette?: BodyDiagramPalette;
122
+ overlayStyle?: BodyDiagramOverlayStyle;
123
+ intensityByMuscle?: Record<string, BodyDiagramIntensity>;
124
+ };
125
+ declare const BodyDiagram: ({ view, svgMarkupByView, baseImageByView, selectedMuscles, onSelectedChange, debugForceVisible, interactionMode, palette, overlayStyle, intensityByMuscle, }: BodyDiagramProps) => react_jsx_runtime.JSX.Element;
126
+
127
+ export { BodyDiagram, type BodyDiagramIntensity, type BodyDiagramOverlayStyle, type BodyDiagramPalette, type BodyDiagramProps, type BodyDiagramView, type Colors, type Radius, type Shadows, type Spacing, type Typography, colors, radius, shadows, spacing, typography };
@@ -0,0 +1,127 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ declare const colors: {
4
+ readonly primary: "#FDC026";
5
+ readonly accent: "#6AABDE";
6
+ readonly bodyDiagram: {
7
+ readonly muscleFill: "#FFBF00";
8
+ readonly muscleStroke: "#FF0090";
9
+ readonly muscleStrokeWidth: 2;
10
+ readonly muscleOpacity: 1;
11
+ };
12
+ readonly background: {
13
+ readonly default: "#161C26";
14
+ readonly muted: "#1C2434";
15
+ readonly highlight: "#383040";
16
+ readonly depth: "#08080A";
17
+ };
18
+ readonly text: {
19
+ readonly primary: "#E8E8E8";
20
+ readonly secondary: "#9B9B9B";
21
+ };
22
+ };
23
+ type Colors = typeof colors;
24
+
25
+ declare const spacing: {
26
+ readonly big: 32;
27
+ readonly default: 24;
28
+ readonly small: 12;
29
+ readonly textHorizontal: 4;
30
+ readonly textVertical: 0;
31
+ };
32
+ type Spacing = typeof spacing;
33
+
34
+ declare const radius: {
35
+ readonly button: 4;
36
+ readonly card: 8;
37
+ readonly input: 4;
38
+ readonly full: 9999;
39
+ };
40
+ type Radius = typeof radius;
41
+
42
+ declare const typography: {
43
+ readonly size: {
44
+ readonly xs: 11;
45
+ readonly sm: 13;
46
+ readonly base: 15;
47
+ readonly md: 17;
48
+ readonly lg: 20;
49
+ readonly xl: 24;
50
+ readonly '2xl': 30;
51
+ readonly '3xl': 36;
52
+ };
53
+ readonly weight: {
54
+ readonly regular: "400";
55
+ readonly medium: "500";
56
+ readonly semibold: "600";
57
+ readonly bold: "700";
58
+ };
59
+ readonly lineHeight: {
60
+ readonly tight: 1.2;
61
+ readonly normal: 1.5;
62
+ readonly relaxed: 1.75;
63
+ };
64
+ };
65
+ type Typography = typeof typography;
66
+
67
+ declare const shadows: {
68
+ readonly sm: {
69
+ readonly shadowColor: "#08080A";
70
+ readonly shadowOffset: {
71
+ readonly width: 0;
72
+ readonly height: 1;
73
+ };
74
+ readonly shadowOpacity: 0.4;
75
+ readonly shadowRadius: 2;
76
+ readonly elevation: 2;
77
+ };
78
+ readonly md: {
79
+ readonly shadowColor: "#08080A";
80
+ readonly shadowOffset: {
81
+ readonly width: 0;
82
+ readonly height: 4;
83
+ };
84
+ readonly shadowOpacity: 0.5;
85
+ readonly shadowRadius: 8;
86
+ readonly elevation: 4;
87
+ };
88
+ readonly lg: {
89
+ readonly shadowColor: "#08080A";
90
+ readonly shadowOffset: {
91
+ readonly width: 0;
92
+ readonly height: 8;
93
+ };
94
+ readonly shadowOpacity: 0.6;
95
+ readonly shadowRadius: 16;
96
+ readonly elevation: 8;
97
+ };
98
+ };
99
+ type Shadows = typeof shadows;
100
+
101
+ type BodyDiagramView = 'front' | 'back';
102
+ type BodyDiagramPalette = {
103
+ muscleFill?: string;
104
+ muscleStroke?: string;
105
+ muscleStrokeWidth?: number;
106
+ muscleOpacity?: number;
107
+ };
108
+ type BodyDiagramOverlayStyle = {
109
+ blur?: number;
110
+ blendMode?: string;
111
+ };
112
+ type BodyDiagramIntensity = 'off' | 'low' | 'medium' | 'high';
113
+ type BodyDiagramProps = {
114
+ view: BodyDiagramView;
115
+ svgMarkupByView: Record<BodyDiagramView, string>;
116
+ baseImageByView?: Partial<Record<BodyDiagramView, string>>;
117
+ selectedMuscles: string[];
118
+ onSelectedChange: (selected: string[]) => void;
119
+ debugForceVisible?: boolean;
120
+ interactionMode?: 'auto' | 'editable' | 'readonly';
121
+ palette?: BodyDiagramPalette;
122
+ overlayStyle?: BodyDiagramOverlayStyle;
123
+ intensityByMuscle?: Record<string, BodyDiagramIntensity>;
124
+ };
125
+ declare const BodyDiagram: ({ view, svgMarkupByView, baseImageByView, selectedMuscles, onSelectedChange, debugForceVisible, interactionMode, palette, overlayStyle, intensityByMuscle, }: BodyDiagramProps) => react_jsx_runtime.JSX.Element;
126
+
127
+ export { BodyDiagram, type BodyDiagramIntensity, type BodyDiagramOverlayStyle, type BodyDiagramPalette, type BodyDiagramProps, type BodyDiagramView, type Colors, type Radius, type Shadows, type Spacing, type Typography, colors, radius, shadows, spacing, typography };
package/dist/index.js ADDED
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/tokens/colors.ts
7
+ var colors = {
8
+ primary: "#FDC026",
9
+ accent: "#6AABDE",
10
+ bodyDiagram: {
11
+ muscleFill: "#FFBF00",
12
+ muscleStroke: "#FF0090",
13
+ muscleStrokeWidth: 2,
14
+ muscleOpacity: 1
15
+ },
16
+ background: {
17
+ default: "#161C26",
18
+ muted: "#1C2434",
19
+ highlight: "#383040",
20
+ depth: "#08080A"
21
+ },
22
+ text: {
23
+ primary: "#E8E8E8",
24
+ secondary: "#9B9B9B"
25
+ }
26
+ };
27
+
28
+ // src/tokens/spacing.ts
29
+ var spacing = {
30
+ big: 32,
31
+ default: 24,
32
+ small: 12,
33
+ textHorizontal: 4,
34
+ textVertical: 0
35
+ };
36
+
37
+ // src/tokens/radius.ts
38
+ var radius = {
39
+ button: 4,
40
+ card: 8,
41
+ input: 4,
42
+ full: 9999
43
+ };
44
+
45
+ // src/tokens/typography.ts
46
+ var typography = {
47
+ size: {
48
+ xs: 11,
49
+ sm: 13,
50
+ base: 15,
51
+ md: 17,
52
+ lg: 20,
53
+ xl: 24,
54
+ "2xl": 30,
55
+ "3xl": 36
56
+ },
57
+ weight: {
58
+ regular: "400",
59
+ medium: "500",
60
+ semibold: "600",
61
+ bold: "700"
62
+ },
63
+ lineHeight: {
64
+ tight: 1.2,
65
+ normal: 1.5,
66
+ relaxed: 1.75
67
+ }
68
+ };
69
+
70
+ // src/tokens/shadows.ts
71
+ var shadows = {
72
+ sm: {
73
+ shadowColor: colors.background.depth,
74
+ shadowOffset: { width: 0, height: 1 },
75
+ shadowOpacity: 0.4,
76
+ shadowRadius: 2,
77
+ elevation: 2
78
+ },
79
+ md: {
80
+ shadowColor: colors.background.depth,
81
+ shadowOffset: { width: 0, height: 4 },
82
+ shadowOpacity: 0.5,
83
+ shadowRadius: 8,
84
+ elevation: 4
85
+ },
86
+ lg: {
87
+ shadowColor: colors.background.depth,
88
+ shadowOffset: { width: 0, height: 8 },
89
+ shadowOpacity: 0.6,
90
+ shadowRadius: 16,
91
+ elevation: 8
92
+ }
93
+ };
94
+
95
+ // src/utils/bodyDiagramState.ts
96
+ var DEFAULT_LEVEL_OPACITY = {
97
+ off: 0,
98
+ low: 0.2,
99
+ medium: 0.5,
100
+ high: 0.9
101
+ };
102
+ var DEFAULT_DEBUG_STYLE = {
103
+ fill: "red",
104
+ stroke: "yellow",
105
+ strokeWidth: 4,
106
+ opacity: 1
107
+ };
108
+ var DEFAULT_PALETTE = {
109
+ muscleFill: colors.bodyDiagram.muscleFill,
110
+ muscleStroke: colors.bodyDiagram.muscleStroke,
111
+ muscleStrokeWidth: colors.bodyDiagram.muscleStrokeWidth,
112
+ muscleOpacity: colors.bodyDiagram.muscleOpacity
113
+ };
114
+ var LEVELS = ["off", "low", "medium", "high"];
115
+ var resolveBodyDiagramTheme = (input = {}) => {
116
+ const palette = { ...DEFAULT_PALETTE, ...input.palette };
117
+ const opacityByLevel = { ...DEFAULT_LEVEL_OPACITY, ...input.levelOpacity };
118
+ const baseOpacity = palette.muscleOpacity ?? 1;
119
+ const levels = LEVELS.reduce(
120
+ (acc, level) => {
121
+ acc[level] = {
122
+ fill: palette.muscleFill ?? "",
123
+ stroke: palette.muscleStroke,
124
+ strokeWidth: palette.muscleStrokeWidth,
125
+ opacity: baseOpacity * (opacityByLevel[level] ?? 1)
126
+ };
127
+ return acc;
128
+ },
129
+ {
130
+ off: { fill: "", opacity: 0 },
131
+ low: { fill: "", opacity: 0 },
132
+ medium: { fill: "", opacity: 0 },
133
+ high: { fill: "", opacity: 0 }
134
+ }
135
+ );
136
+ return {
137
+ levels,
138
+ debug: input.debugStyle ?? DEFAULT_DEBUG_STYLE
139
+ };
140
+ };
141
+ var resolveBodyDiagramMuscleState = (input) => {
142
+ const { muscleKey, selectedMuscles, intensityByMuscle, debugForceVisible } = input;
143
+ const explicitLevel = intensityByMuscle?.[muscleKey];
144
+ let level = "off";
145
+ let source = "defaultOff";
146
+ if (explicitLevel) {
147
+ level = explicitLevel;
148
+ source = "explicit";
149
+ } else if (selectedMuscles.includes(muscleKey)) {
150
+ level = "high";
151
+ source = "selectedFallback";
152
+ }
153
+ const theme = input.theme ?? resolveBodyDiagramTheme({ palette: input.palette });
154
+ const isDebugForced = Boolean(debugForceVisible);
155
+ const style = isDebugForced ? theme.debug ?? DEFAULT_DEBUG_STYLE : theme.levels[level];
156
+ const shouldRender = isDebugForced ? true : level !== "off";
157
+ const interactionMode = input.interactionMode ?? "auto";
158
+ const interactive = interactionMode === "readonly" ? false : interactionMode === "editable" ? true : level !== "off";
159
+ return {
160
+ key: muscleKey,
161
+ level,
162
+ interactive,
163
+ style,
164
+ source,
165
+ shouldRender,
166
+ isDebugForced
167
+ };
168
+ };
169
+ var BodyDiagram = ({
170
+ view,
171
+ svgMarkupByView,
172
+ baseImageByView,
173
+ selectedMuscles,
174
+ onSelectedChange,
175
+ debugForceVisible = false,
176
+ interactionMode = "auto",
177
+ palette,
178
+ overlayStyle,
179
+ intensityByMuscle
180
+ }) => {
181
+ const containerRef = react.useRef(null);
182
+ const svgMarkup = svgMarkupByView[view] || "";
183
+ const baseImage = baseImageByView?.[view] || "";
184
+ const handleClick = react.useCallback(
185
+ (event) => {
186
+ if (interactionMode === "readonly") return;
187
+ const target = event.target;
188
+ const element = target.closest("[data-muscle-key]");
189
+ const key = element?.getAttribute("data-muscle-key");
190
+ if (!key) return;
191
+ const next = new Set(selectedMuscles);
192
+ if (next.has(key)) {
193
+ next.delete(key);
194
+ } else {
195
+ next.add(key);
196
+ }
197
+ onSelectedChange(Array.from(next));
198
+ },
199
+ [interactionMode, onSelectedChange, selectedMuscles]
200
+ );
201
+ react.useEffect(() => {
202
+ const container = containerRef.current;
203
+ if (!container) return;
204
+ const svg = container.querySelector("svg");
205
+ if (!svg) return;
206
+ svg.style.width = "100%";
207
+ svg.style.height = "100%";
208
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
209
+ const elements = svg.querySelectorAll("[data-muscle-key]");
210
+ const theme = resolveBodyDiagramTheme({ palette });
211
+ elements.forEach((el) => {
212
+ const element = el;
213
+ const key = element.getAttribute("data-muscle-key");
214
+ if (!key) return;
215
+ const resolved = resolveBodyDiagramMuscleState({
216
+ muscleKey: key,
217
+ selectedMuscles,
218
+ intensityByMuscle,
219
+ palette,
220
+ debugForceVisible,
221
+ theme,
222
+ interactionMode
223
+ });
224
+ if (resolved.isDebugForced) {
225
+ element.removeAttribute("filter");
226
+ element.removeAttribute("mask");
227
+ element.removeAttribute("clip-path");
228
+ element.removeAttribute("style");
229
+ }
230
+ const isOff = resolved.level === "off";
231
+ const forceHitArea = isOff && !resolved.isDebugForced;
232
+ if (forceHitArea) {
233
+ element.setAttribute("fill", "rgba(0,0,0,0)");
234
+ } else if (resolved.style.fill) {
235
+ element.setAttribute("fill", resolved.style.fill);
236
+ }
237
+ if (resolved.style.stroke) {
238
+ element.setAttribute("stroke", resolved.style.stroke);
239
+ }
240
+ if (resolved.style.strokeWidth != null) {
241
+ element.setAttribute("stroke-width", String(resolved.style.strokeWidth));
242
+ }
243
+ element.setAttribute("opacity", String(resolved.style.opacity));
244
+ element.style.pointerEvents = "auto";
245
+ });
246
+ }, [selectedMuscles, svgMarkup, debugForceVisible, intensityByMuscle, palette, interactionMode]);
247
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyle, children: [
248
+ baseImage ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: baseImage, alt: "", style: baseImageStyle }) : null,
249
+ /* @__PURE__ */ jsxRuntime.jsx(
250
+ "div",
251
+ {
252
+ ref: containerRef,
253
+ style: {
254
+ position: "absolute",
255
+ inset: 0,
256
+ width: "100%",
257
+ height: "100%",
258
+ filter: overlayStyle?.blur ? `blur(${overlayStyle.blur}px)` : void 0,
259
+ mixBlendMode: overlayStyle?.blendMode
260
+ },
261
+ onClick: handleClick,
262
+ dangerouslySetInnerHTML: { __html: svgMarkup }
263
+ }
264
+ )
265
+ ] });
266
+ };
267
+ var containerStyle = {
268
+ display: "flex",
269
+ width: "100%",
270
+ height: "100%",
271
+ position: "relative",
272
+ overflow: "hidden"
273
+ };
274
+ var baseImageStyle = {
275
+ position: "absolute",
276
+ inset: 0,
277
+ width: "100%",
278
+ height: "100%",
279
+ objectFit: "contain"
280
+ };
281
+
282
+ exports.BodyDiagram = BodyDiagram;
283
+ exports.colors = colors;
284
+ exports.radius = radius;
285
+ exports.shadows = shadows;
286
+ exports.spacing = spacing;
287
+ exports.typography = typography;
package/dist/index.mjs ADDED
@@ -0,0 +1,280 @@
1
+ import { useRef, useCallback, useEffect } from 'react';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/tokens/colors.ts
5
+ var colors = {
6
+ primary: "#FDC026",
7
+ accent: "#6AABDE",
8
+ bodyDiagram: {
9
+ muscleFill: "#FFBF00",
10
+ muscleStroke: "#FF0090",
11
+ muscleStrokeWidth: 2,
12
+ muscleOpacity: 1
13
+ },
14
+ background: {
15
+ default: "#161C26",
16
+ muted: "#1C2434",
17
+ highlight: "#383040",
18
+ depth: "#08080A"
19
+ },
20
+ text: {
21
+ primary: "#E8E8E8",
22
+ secondary: "#9B9B9B"
23
+ }
24
+ };
25
+
26
+ // src/tokens/spacing.ts
27
+ var spacing = {
28
+ big: 32,
29
+ default: 24,
30
+ small: 12,
31
+ textHorizontal: 4,
32
+ textVertical: 0
33
+ };
34
+
35
+ // src/tokens/radius.ts
36
+ var radius = {
37
+ button: 4,
38
+ card: 8,
39
+ input: 4,
40
+ full: 9999
41
+ };
42
+
43
+ // src/tokens/typography.ts
44
+ var typography = {
45
+ size: {
46
+ xs: 11,
47
+ sm: 13,
48
+ base: 15,
49
+ md: 17,
50
+ lg: 20,
51
+ xl: 24,
52
+ "2xl": 30,
53
+ "3xl": 36
54
+ },
55
+ weight: {
56
+ regular: "400",
57
+ medium: "500",
58
+ semibold: "600",
59
+ bold: "700"
60
+ },
61
+ lineHeight: {
62
+ tight: 1.2,
63
+ normal: 1.5,
64
+ relaxed: 1.75
65
+ }
66
+ };
67
+
68
+ // src/tokens/shadows.ts
69
+ var shadows = {
70
+ sm: {
71
+ shadowColor: colors.background.depth,
72
+ shadowOffset: { width: 0, height: 1 },
73
+ shadowOpacity: 0.4,
74
+ shadowRadius: 2,
75
+ elevation: 2
76
+ },
77
+ md: {
78
+ shadowColor: colors.background.depth,
79
+ shadowOffset: { width: 0, height: 4 },
80
+ shadowOpacity: 0.5,
81
+ shadowRadius: 8,
82
+ elevation: 4
83
+ },
84
+ lg: {
85
+ shadowColor: colors.background.depth,
86
+ shadowOffset: { width: 0, height: 8 },
87
+ shadowOpacity: 0.6,
88
+ shadowRadius: 16,
89
+ elevation: 8
90
+ }
91
+ };
92
+
93
+ // src/utils/bodyDiagramState.ts
94
+ var DEFAULT_LEVEL_OPACITY = {
95
+ off: 0,
96
+ low: 0.2,
97
+ medium: 0.5,
98
+ high: 0.9
99
+ };
100
+ var DEFAULT_DEBUG_STYLE = {
101
+ fill: "red",
102
+ stroke: "yellow",
103
+ strokeWidth: 4,
104
+ opacity: 1
105
+ };
106
+ var DEFAULT_PALETTE = {
107
+ muscleFill: colors.bodyDiagram.muscleFill,
108
+ muscleStroke: colors.bodyDiagram.muscleStroke,
109
+ muscleStrokeWidth: colors.bodyDiagram.muscleStrokeWidth,
110
+ muscleOpacity: colors.bodyDiagram.muscleOpacity
111
+ };
112
+ var LEVELS = ["off", "low", "medium", "high"];
113
+ var resolveBodyDiagramTheme = (input = {}) => {
114
+ const palette = { ...DEFAULT_PALETTE, ...input.palette };
115
+ const opacityByLevel = { ...DEFAULT_LEVEL_OPACITY, ...input.levelOpacity };
116
+ const baseOpacity = palette.muscleOpacity ?? 1;
117
+ const levels = LEVELS.reduce(
118
+ (acc, level) => {
119
+ acc[level] = {
120
+ fill: palette.muscleFill ?? "",
121
+ stroke: palette.muscleStroke,
122
+ strokeWidth: palette.muscleStrokeWidth,
123
+ opacity: baseOpacity * (opacityByLevel[level] ?? 1)
124
+ };
125
+ return acc;
126
+ },
127
+ {
128
+ off: { fill: "", opacity: 0 },
129
+ low: { fill: "", opacity: 0 },
130
+ medium: { fill: "", opacity: 0 },
131
+ high: { fill: "", opacity: 0 }
132
+ }
133
+ );
134
+ return {
135
+ levels,
136
+ debug: input.debugStyle ?? DEFAULT_DEBUG_STYLE
137
+ };
138
+ };
139
+ var resolveBodyDiagramMuscleState = (input) => {
140
+ const { muscleKey, selectedMuscles, intensityByMuscle, debugForceVisible } = input;
141
+ const explicitLevel = intensityByMuscle?.[muscleKey];
142
+ let level = "off";
143
+ let source = "defaultOff";
144
+ if (explicitLevel) {
145
+ level = explicitLevel;
146
+ source = "explicit";
147
+ } else if (selectedMuscles.includes(muscleKey)) {
148
+ level = "high";
149
+ source = "selectedFallback";
150
+ }
151
+ const theme = input.theme ?? resolveBodyDiagramTheme({ palette: input.palette });
152
+ const isDebugForced = Boolean(debugForceVisible);
153
+ const style = isDebugForced ? theme.debug ?? DEFAULT_DEBUG_STYLE : theme.levels[level];
154
+ const shouldRender = isDebugForced ? true : level !== "off";
155
+ const interactionMode = input.interactionMode ?? "auto";
156
+ const interactive = interactionMode === "readonly" ? false : interactionMode === "editable" ? true : level !== "off";
157
+ return {
158
+ key: muscleKey,
159
+ level,
160
+ interactive,
161
+ style,
162
+ source,
163
+ shouldRender,
164
+ isDebugForced
165
+ };
166
+ };
167
+ var BodyDiagram = ({
168
+ view,
169
+ svgMarkupByView,
170
+ baseImageByView,
171
+ selectedMuscles,
172
+ onSelectedChange,
173
+ debugForceVisible = false,
174
+ interactionMode = "auto",
175
+ palette,
176
+ overlayStyle,
177
+ intensityByMuscle
178
+ }) => {
179
+ const containerRef = useRef(null);
180
+ const svgMarkup = svgMarkupByView[view] || "";
181
+ const baseImage = baseImageByView?.[view] || "";
182
+ const handleClick = useCallback(
183
+ (event) => {
184
+ if (interactionMode === "readonly") return;
185
+ const target = event.target;
186
+ const element = target.closest("[data-muscle-key]");
187
+ const key = element?.getAttribute("data-muscle-key");
188
+ if (!key) return;
189
+ const next = new Set(selectedMuscles);
190
+ if (next.has(key)) {
191
+ next.delete(key);
192
+ } else {
193
+ next.add(key);
194
+ }
195
+ onSelectedChange(Array.from(next));
196
+ },
197
+ [interactionMode, onSelectedChange, selectedMuscles]
198
+ );
199
+ useEffect(() => {
200
+ const container = containerRef.current;
201
+ if (!container) return;
202
+ const svg = container.querySelector("svg");
203
+ if (!svg) return;
204
+ svg.style.width = "100%";
205
+ svg.style.height = "100%";
206
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
207
+ const elements = svg.querySelectorAll("[data-muscle-key]");
208
+ const theme = resolveBodyDiagramTheme({ palette });
209
+ elements.forEach((el) => {
210
+ const element = el;
211
+ const key = element.getAttribute("data-muscle-key");
212
+ if (!key) return;
213
+ const resolved = resolveBodyDiagramMuscleState({
214
+ muscleKey: key,
215
+ selectedMuscles,
216
+ intensityByMuscle,
217
+ palette,
218
+ debugForceVisible,
219
+ theme,
220
+ interactionMode
221
+ });
222
+ if (resolved.isDebugForced) {
223
+ element.removeAttribute("filter");
224
+ element.removeAttribute("mask");
225
+ element.removeAttribute("clip-path");
226
+ element.removeAttribute("style");
227
+ }
228
+ const isOff = resolved.level === "off";
229
+ const forceHitArea = isOff && !resolved.isDebugForced;
230
+ if (forceHitArea) {
231
+ element.setAttribute("fill", "rgba(0,0,0,0)");
232
+ } else if (resolved.style.fill) {
233
+ element.setAttribute("fill", resolved.style.fill);
234
+ }
235
+ if (resolved.style.stroke) {
236
+ element.setAttribute("stroke", resolved.style.stroke);
237
+ }
238
+ if (resolved.style.strokeWidth != null) {
239
+ element.setAttribute("stroke-width", String(resolved.style.strokeWidth));
240
+ }
241
+ element.setAttribute("opacity", String(resolved.style.opacity));
242
+ element.style.pointerEvents = "auto";
243
+ });
244
+ }, [selectedMuscles, svgMarkup, debugForceVisible, intensityByMuscle, palette, interactionMode]);
245
+ return /* @__PURE__ */ jsxs("div", { style: containerStyle, children: [
246
+ baseImage ? /* @__PURE__ */ jsx("img", { src: baseImage, alt: "", style: baseImageStyle }) : null,
247
+ /* @__PURE__ */ jsx(
248
+ "div",
249
+ {
250
+ ref: containerRef,
251
+ style: {
252
+ position: "absolute",
253
+ inset: 0,
254
+ width: "100%",
255
+ height: "100%",
256
+ filter: overlayStyle?.blur ? `blur(${overlayStyle.blur}px)` : void 0,
257
+ mixBlendMode: overlayStyle?.blendMode
258
+ },
259
+ onClick: handleClick,
260
+ dangerouslySetInnerHTML: { __html: svgMarkup }
261
+ }
262
+ )
263
+ ] });
264
+ };
265
+ var containerStyle = {
266
+ display: "flex",
267
+ width: "100%",
268
+ height: "100%",
269
+ position: "relative",
270
+ overflow: "hidden"
271
+ };
272
+ var baseImageStyle = {
273
+ position: "absolute",
274
+ inset: 0,
275
+ width: "100%",
276
+ height: "100%",
277
+ objectFit: "contain"
278
+ };
279
+
280
+ export { BodyDiagram, colors, radius, shadows, spacing, typography };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@jlunamena/design-system",
3
+ "version": "1.0.0",
4
+ "description": "PT Design System",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "storybook": "storybook dev -p 6006",
22
+ "build-storybook": "storybook build",
23
+ "generate:exports": "node scripts/generate-exports.mjs"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=18.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@storybook/addon-essentials": "^8.6.0",
30
+ "@storybook/react": "^8.6.0",
31
+ "@storybook/react-vite": "^8.6.0",
32
+ "@types/react": "^19.1.0",
33
+ "@types/react-dom": "^19.1.0",
34
+ "@vitejs/plugin-react": "^4.3.0",
35
+ "esbuild": "^0.28.0",
36
+ "react": "^19.1.0",
37
+ "react-dom": "^19.1.0",
38
+ "storybook": "^8.6.0",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "~5.9.2",
41
+ "vite": "^5.0.0"
42
+ }
43
+ }