-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfigma_sync.html
More file actions
300 lines (266 loc) · 16.9 KB
/
figma_sync.html
File metadata and controls
300 lines (266 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jeff Halpin - Figma Design Token Sync Tool</title>
<link rel="icon" type="image/x-icon" href="/projects/portfolio/favicon.ico?v=2" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kaisei+Opti&family=Lato:wght@400;700&family=Playfair+Display:wght@400;700&family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Kaisei Opti', serif;
}
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'off-white': '#FFFEF9',
'white': '#FFFFFF',
'near-black': '#181818',
'light-gray': '#F7F7F7',
'aquamarine': '#61EDD8',
'accent': '#079E87',
'text-primary': '#181818',
'text-secondary': '#DFE1E3',
'accent-primary': '#61EDD8',
'background-base': '#FFFFFF',
'mid-gray': '#DFE1E3',
'surface-default': '#FFFEF9',
'border-subtle': '#F7F7F7',
'text-tertiary': '#526AC2',
},
fontFamily: {
'kaisei': ['Kaisei Opti', 'serif'],
'lato': ['Lato', 'sans-serif'],
'playfair': ['Playfair Display', 'serif'],
'poppins': ['Poppins', 'sans-serif'],
}
}
}
}
</script>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PLH7LQ');</script>
<!-- End Google Tag Manager -->
</head>
<body class="bg-[#FFFEF9] font-kaisei min-h-screen relative">
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PLH7LQ"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<!-- HEADER -->
<header class="bg-[#FFFEF9] border-b-0 md:border-b md:border-neutral-200 sticky top-0 z-40 h-[60px]">
<div class="flex items-center justify-between gap-4 max-w-7xl mx-auto h-full px-4">
<a href="index.html" class="font-kaisei text-2xl text-black font-medium hover:text-accent transition-colors">Jeff Halpin</a>
<nav class="hidden md:flex items-center gap-6">
<a href="index.html#about" class="text-sm text-black hover:text-accent transition-colors">About</a>
<a href="index.html#work" class="text-sm text-black hover:text-accent transition-colors">Work</a>
<a href="index.html#approach" class="text-sm text-black hover:text-accent transition-colors">Approach</a>
<a href="contact.html" class="text-sm text-black hover:text-accent transition-colors">Contact</a>
</nav>
<button id="menu-toggle" class="md:hidden w-[35px] h-[44px] flex flex-col items-center justify-center gap-[8px] p-2" aria-label="Toggle menu">
<span class="w-full h-[2px] bg-black"></span>
<span class="w-full h-[2px] bg-black"></span>
</button>
</div>
</header>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden fixed inset-0 z-50 bg-black/50 md:hidden">
<div class="bg-[#FFFEF9] w-4/5 h-full p-6 flex flex-col gap-6">
<button id="menu-close" class="self-end text-2xl" aria-label="Close menu">×</button>
<nav class="flex flex-col gap-4">
<a href="index.html#about" class="text-lg text-black hover:text-accent transition-colors">About</a>
<a href="index.html#work" class="text-lg text-black hover:text-accent transition-colors">Work</a>
<a href="index.html#approach" class="text-lg text-black hover:text-accent transition-colors">Approach</a>
<a href="contact.html" class="text-lg text-black hover:text-accent transition-colors">Contact</a>
</nav>
</div>
</div>
<!-- HERO IMAGE (full-bleed) -->
<section>
<img src="images/figma_sync_loop_448.gif" alt="Figma Design Token Sync Tool" class="w-full max-w-[1440px] mx-auto">
</section>
<!-- PROJECT TITLE & SHORT DESCRIPTION -->
<section class="px-4 md:px-0 pt-[80px] pb-[48px]">
<div class="max-w-[1200px] mx-auto">
<h1 class="text-[40px] md:text-[48px] leading-tight text-text-primary font-normal mb-6">AI Development, UX Engineering: Figma Design Token Sync Tool</h1>
<p class="text-lg text-text-primary leading-relaxed">
An automated workflow that syncs design tokens from Figma to code, keeping the design system and front-end implementation in perfect alignment.
</p>
</div>
</section>
<!-- PROJECT CONTENT & METADATA -->
<section class="px-4 md:px-0">
<div class="max-w-[1200px] mx-auto flex flex-col md:flex-row gap-[32px] md:gap-[64px]">
<!-- Left: Main Content -->
<div class="md:w-[750px] flex flex-col gap-[32px]">
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">The Challenge</h3>
<p class="text-base text-text-primary leading-relaxed mb-4">Keeping design tokens synchronized between Figma and code is a persistent challenge in design-development workflows. Manual updates are error-prone, time-consuming, and often lead to drift between what designers specify and what gets implemented. A color change in Figma might take days to propagate to the codebase, or worse, get transcribed incorrectly.</p>
<p class="text-base text-text-primary leading-relaxed">For this portfolio project, I wanted a single source of truth: update colors in Figma, run a command, and have the code update automatically. No copy-pasting hex values, no version mismatches.</p>
</div>
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">My Role</h3>
<p class="text-base text-text-primary leading-relaxed">Solo developer and designer. I scoped the requirements, evaluated existing solutions, designed the workflow, and built the tool.</p>
</div>
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">Research & Discovery</h3>
<p class="text-base text-text-primary leading-relaxed mb-4">I evaluated existing solutions including Tokens Studio, Style Dictionary, and Figma's native Variables. These tools are powerful but optimized for enterprise workflows: multi-platform output, version control integration, and team collaboration features. For a solo portfolio project with a single output target (Tailwind CSS), they introduced unnecessary complexity.</p>
<p class="text-base text-text-primary leading-relaxed">I needed something simpler: pull colors from Figma, update Tailwind config, done. No plugins, no build pipeline, no dependencies beyond Node.js.</p>
</div>
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">Key Design Decisions</h3>
<p class="text-base text-text-primary leading-relaxed mb-4"><strong>1. Direct API integration over plugin abstraction</strong><br>
I built a Node.js script that connects to the Figma API directly, extracts all published color styles from my design system file, and updates the Tailwind CSS configuration. During development, I discovered that Figma's API requires the X-Figma-Token header rather than the more common Authorization: Bearer pattern. This debugging experience reinforced the importance of reading API documentation carefully and testing authentication flows early.</p>
<p class="text-base text-text-primary leading-relaxed mb-4"><strong>2. Zero external dependencies</strong><br>
The tool has no dependencies beyond Node.js itself, making it portable and easy to integrate into any project. It supports both published library colors and document-level style extraction as a fallback.</p>
<p class="text-base text-text-primary leading-relaxed"><strong>3. Tailwind-native output</strong><br>
Rather than generating intermediate JSON that requires further transformation, the script outputs a Tailwind-compatible color configuration directly. This eliminates an entire step from the workflow and reduces potential points of failure.</p>
</div>
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">Outcome</h3>
<p class="text-base text-text-primary leading-relaxed mb-4">The tool actively powers this portfolio. Every color on jeffhalpin.com syncs directly from my Figma design system file. When I adjust a color in Figma, a single terminal command updates the Tailwind configuration across the entire site.</p>
<p class="text-base text-text-primary leading-relaxed mb-4">The real value is time saved on consistency maintenance. Without automation, keeping hex values aligned between design files and code requires manual checking and transcription. That work now takes seconds instead of minutes, and eliminates transcription errors entirely.</p>
<p class="text-base text-text-primary leading-relaxed">The tool is open source and available on GitHub.</p>
</div>
<div>
<h3 class="text-2xl text-text-primary font-normal mb-4">Reflection</h3>
<p class="text-base text-text-primary leading-relaxed mb-4">Existing tools like Tokens Studio and Style Dictionary are powerful, but they come with learning curves and configuration overhead. For a focused use case with a clear output target, building a custom solution taught me more about the underlying systems than configuring someone else's tool would have.</p>
<p class="text-base text-text-primary leading-relaxed mb-4">Understanding the Figma API directly, rather than through a plugin abstraction, gave me insight into how design data is structured and accessed. That knowledge transfers to future projects in ways that plugin configuration does not.</p>
<p class="text-base text-text-primary leading-relaxed">Next steps include expanding the tool to sync typography and spacing tokens.</p>
</div>
</div>
<!-- Right: Metadata Sidebar -->
<div class="md:w-[360px] md:flex-shrink-0 p-[24px] flex flex-col gap-[24px] h-fit">
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Client</h4>
<p class="text-base text-text-primary">Personal Project</p>
</div>
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Discipline</h4>
<p class="text-base text-text-primary">Design Systems<br>Automation<br>API Integration<br>Front-end Development</p>
</div>
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Role</h4>
<p class="text-base text-text-primary">Developer / UX Engineer</p>
</div>
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Year</h4>
<p class="text-base text-text-primary">2025</p>
</div>
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Technologies</h4>
<p class="text-base text-text-primary">Node.js<br>Figma API<br>Tailwind CSS<br>JavaScript</p>
</div>
<div>
<h4 class="text-sm text-accent uppercase mb-2 tracking-wide">Deliverables</h4>
<p class="text-base text-text-primary">Sync Script<br>Documentation<br>GitHub Repository</p>
</div>
</div>
</div>
</section>
<!-- EXTERNAL LINK -->
<section class="px-4 md:px-0 py-8 mb-12">
<div class="max-w-[1200px] mx-auto">
<a href="https://github.com/JDHalpin/figma-design-tokens" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 text-accent hover:underline text-lg">
View on GitHub →
</a>
</div>
</section>
<!-- BACK TO WORK -->
<section class="px-4 md:px-0 py-8 mb-12">
<div class="max-w-[1200px] mx-auto">
<a href="index.html" class="inline-flex items-center gap-2 text-accent hover:underline font-lato">
← Back to Work
</a>
</div>
</section>
<!-- FOOTER -->
<footer class="bg-[#FFFEF9] border-t border-[#F6F6F6] px-4 pt-6 pb-16 md:pb-8 flex flex-col items-center justify-center gap-4 text-sm text-black text-center">
<p>© 2026 Jeff Halpin</p>
<div class="flex items-center justify-center gap-6">
<a href="https://www.linkedin.com/in/jeffrey-halpin/" target="_blank" rel="noopener noreferrer"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
</svg></a>
<a href="https://github.com/JDHalpin" target="_blank" rel="noopener noreferrer"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
</svg></a>
<a href="mailto:jeff.halpin@gmail.com"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
</svg></a>
</div>
</footer>
<!-- LIGHTBOX MODAL -->
<div id="lightbox" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
<button id="lightbox-close" class="absolute top-4 right-4 text-white text-3xl hover:text-accent">×</button>
<img id="lightbox-img" src="" alt="" class="max-w-full max-h-[90vh] rounded-lg">
</div>
<script>
// Mobile menu logic
const menuToggle = document.getElementById('menu-toggle');
const menuClose = document.getElementById('menu-close');
const mobileMenu = document.getElementById('mobile-menu');
menuToggle?.addEventListener('click', () => {
mobileMenu.classList.remove('hidden');
document.body.classList.add('overflow-hidden');
});
menuClose?.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
});
mobileMenu?.addEventListener('click', (e) => {
if (e.target === mobileMenu) {
mobileMenu.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
});
document.querySelectorAll('#mobile-menu a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
});
});
// Lightbox functionality
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxClose = document.getElementById('lightbox-close');
// Open lightbox on image click
document.querySelectorAll('.lightbox-trigger').forEach(img => {
img.addEventListener('click', () => {
lightboxImg.src = img.src;
lightboxImg.alt = img.alt;
lightbox.classList.remove('hidden');
document.body.classList.add('overflow-hidden');
});
});
// Close lightbox on close button click
lightboxClose?.addEventListener('click', () => {
lightbox.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
});
// Close lightbox on backdrop click
lightbox?.addEventListener('click', (e) => {
if (e.target === lightbox) {
lightbox.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
});
// Close lightbox on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !lightbox.classList.contains('hidden')) {
lightbox.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
});
</script>
</body>
</html>