1+ /* eslint-disable @typescript-eslint/no-explicit-any */
12import fs from "fs" ;
23import path from "path" ;
34
45const FIGMA_VARIABLE_TOKEN = process . env . FIGMA_VARIABLE_TOKEN ;
56const FIGMA_FILE_KEY = process . env . FIGMA_FILE_KEY ;
67const OUTPUT_DIR = "src/variables" ;
78
8- // Must match exactly the collection names in your Figma file
99const WANTED_COLLECTIONS = new Set ( [
1010 "1. Semantic colors" ,
1111 "1.1 Base colors" ,
@@ -15,7 +15,6 @@ const WANTED_COLLECTIONS = new Set([
1515 "3.1 Base fonts" ,
1616] ) ;
1717
18- // Themes
1918const THEME_MAP : Record < string , { folder : string ; fileSuffix : string } > = {
2019 default : { folder : "default" , fileSuffix : "default" } ,
2120 light : { folder : "default" , fileSuffix : "default" } ,
@@ -30,9 +29,6 @@ const RESPONSIVE_MEDIA: Record<string, string> = {
3029 mobile : "(max-width: 48rem)" ,
3130} ;
3231
33- /* ==========================================================================
34- UTILITIES
35- ========================================================================== */
3632function sortCssVars ( list : string [ ] ) {
3733 list . sort ( ( a , b ) => {
3834 const nameA = a . trim ( ) . split ( ":" ) [ 0 ] ;
@@ -52,10 +48,6 @@ function kebab(str: string): string {
5248 return str . replace ( / [ ^ a - z A - Z 0 - 9 ] + / g, "-" ) . replace ( / ^ - + | - + $ / g, "" ) . toLowerCase ( ) ;
5349}
5450
55- /* ==========================================================================
56- THEME HELPERS – DATA-DRIVEN MAGIC
57- ========================================================================== */
58- // Find theme config by checking if the mode name contains any key from THEME_MAP
5951function getThemeConfigFromMode ( modeName : string ) : { folder : string ; fileSuffix : string } | undefined {
6052 const lower = modeName . toLowerCase ( ) ;
6153 for ( const [ key , config ] of Object . entries ( THEME_MAP ) ) {
@@ -64,28 +56,23 @@ function getThemeConfigFromMode(modeName: string): { folder: string; fileSuffix:
6456 return undefined ;
6557}
6658
67- // Returns true for "brand" themes (muis, rit, etc.) – used to skip responsive overrides
6859function isBrandTheme ( modeName : string ) : boolean {
6960 const config = getThemeConfigFromMode ( modeName ) ;
7061 if ( ! config ) return false ;
7162 const lower = modeName . toLowerCase ( ) ;
7263 return ! ( lower . includes ( "default" ) || lower . includes ( "light" ) || lower . includes ( "dark" ) ) ;
7364}
7465
75- /* ==========================================================================
76- FIGMA API
77- ========================================================================== */
78- async function fetchFigmaVariables ( ) {
66+ async function fetchFigmaVariables ( ) : Promise < FigmaVariablesResponse > {
7967 const res = await fetch ( `https://api.figma.com/v1/files/${ FIGMA_FILE_KEY } /variables/local` , {
8068 headers : { "X-Figma-Token" : FIGMA_VARIABLE_TOKEN ?? "" } ,
8169 } ) ;
70+
8271 if ( ! res . ok ) throw new Error ( `Figma API error: ${ res . status } ` ) ;
83- return await res . json ( ) ;
72+
73+ return ( await res . json ( ) ) as FigmaVariablesResponse ;
8474}
8575
86- /* ==========================================================================
87- UNIT & CONVERSION LOGIC
88- ========================================================================== */
8976function getUnit ( collectionName : string ) : "px" | "rem" | null {
9077 const l = collectionName . toLowerCase ( ) ;
9178 if ( l . includes ( "dimension" ) ) return "px" ;
@@ -130,13 +117,11 @@ function resolveValue(
130117) : string {
131118 if ( ! raw ) return "0" ;
132119
133- // Alias
134120 if ( raw && typeof raw === "object" && ( raw . type === "VARIABLE_ALIAS" || ( raw . id && ! ( "value" in raw ) ) ) ) {
135121 const cssVar = aliasMap [ raw . id ] ;
136122 return cssVar ? `var(${ cssVar } )` : "0" ;
137123 }
138124
139- // Color
140125 if ( raw && typeof raw === "object" && "r" in raw ) {
141126 const v = "value" in raw ? raw . value : raw ;
142127 const { r, g, b, a = 1 } = v ;
@@ -148,31 +133,24 @@ function resolveValue(
148133 const lowerName = varName . toLowerCase ( ) ;
149134
150135 if ( typeof value === "number" ) {
151- // ─────────────────────── BASE VARIABLES ───────────────────────
152136 if ( convertToRemForBase ) {
153- // 1. Font-weight → always unitless
154137 if ( lowerName . includes ( "weight" ) ) {
155138 return value . toString ( ) ;
156139 }
157140
158- // 2. Things we explicitly want in rem
159141 if ( shouldConvertBaseToRem ( varName ) ) {
160142 return pxToRem ( value ) ;
161143 }
162144
163- // 3. Everything else in base → px (containers, breakpoints, etc.)
164145 return `${ Math . round ( value * 100 ) / 100 } px` ;
165146 }
166147
167- // ─────────────────────── SEMANTIC LAYERS ───────────────────────
168- // Fonts collection
169148 if ( lowerName . includes ( "font" ) || unit === "rem" ) {
170- if ( lowerName . includes ( "weight" ) ) return value . toString ( ) ; // ← unitless
171- if ( isLineHeight ( varName ) ) return pxToRem ( value ) ; // ← rem
172- return pxToRem ( value ) ; // ← rem (font-size, etc.)
149+ if ( lowerName . includes ( "weight" ) ) return value . toString ( ) ;
150+ if ( isLineHeight ( varName ) ) return pxToRem ( value ) ;
151+ return pxToRem ( value ) ;
173152 }
174153
175- // Dimensions collection
176154 if ( unit === "px" || lowerName . includes ( "radius" ) || lowerName . includes ( "border" ) ) {
177155 return `${ Math . round ( value * 100 ) / 100 } px` ;
178156 }
@@ -183,9 +161,6 @@ function resolveValue(
183161 return value !== undefined ? String ( value ) : "0" ;
184162}
185163
186- /* ==========================================================================
187- TYPES & MAIN
188- ========================================================================== */
189164interface FigmaVariable {
190165 name : string ;
191166 resolvedValuesByMode ?: Record < string , any > ;
@@ -197,6 +172,12 @@ interface FigmaCollection {
197172 variableIds : string [ ] ;
198173 defaultModeId ?: string ;
199174}
175+ interface FigmaVariablesResponse {
176+ meta : {
177+ variables : Record < string , FigmaVariable > ;
178+ variableCollections : Record < string , FigmaCollection > ;
179+ } ;
180+ }
200181
201182async function run ( ) {
202183 const data = await fetchFigmaVariables ( ) ;
@@ -212,7 +193,6 @@ async function run() {
212193 WANTED_COLLECTIONS . has ( c . name . trim ( ) )
213194 ) ;
214195
215- /* ———————————————————————— 1. BASE VARIABLES ———————————————————————— */
216196 const baseLines : string [ ] = [ ] ;
217197 for ( const coll of wantedColls . filter ( c => c . name . includes ( "Base" ) ) ) {
218198 const modeId = coll . defaultModeId ?? coll . modes [ 0 ] ?. modeId ;
@@ -236,14 +216,12 @@ async function run() {
236216 fs . writeFileSync ( path . join ( OUTPUT_DIR , "_base-variables.scss" ) , css ) ;
237217 }
238218
239- /* ———————————————————————— 2. SEMANTIC VARIABLES ———————————————————————— */
240219 for ( const coll of wantedColls . filter ( c => ! c . name . includes ( "Base" ) ) ) {
241220 const unit = getUnit ( coll . name ) ;
242221 const isFonts = coll . name === "3. Semantic fonts" ;
243222 const isColors = coll . name === "1. Semantic colors" ;
244223 const isDimensions = coll . name === "2. Semantic dimensions" ;
245224
246- /* ——— Responsive Fonts (default/light/dark only) ——— */
247225 if ( isFonts ) {
248226 const desktopLines : string [ ] = [ ] ;
249227 const tabletLines : string [ ] = [ ] ;
@@ -252,7 +230,6 @@ async function run() {
252230 for ( const mode of coll . modes ) {
253231 const lower = mode . name . toLowerCase ( ) ;
254232
255- // Skip brand themes – they get their own full theme files
256233 if ( isBrandTheme ( mode . name ) ) continue ;
257234
258235 const target = lower . includes ( "mobile" ) ? mobileLines
@@ -281,10 +258,9 @@ async function run() {
281258 }
282259 }
283260
284- /* ——— Theme-specific files ——— */
285261 for ( const mode of coll . modes ) {
286262 const themeConfig = getThemeConfigFromMode ( mode . name ) ;
287- if ( ! themeConfig ) continue ; // not a recognized theme → skip
263+ if ( ! themeConfig ) continue ;
288264
289265 const lines : string [ ] = [ ] ;
290266 for ( const varId of coll . variableIds ) {
@@ -308,7 +284,6 @@ async function run() {
308284 fs . writeFileSync ( path . join ( dir , fileName ) , css ) ;
309285 }
310286
311- /* ——— Responsive Dimensions ——— */
312287 if ( isDimensions ) {
313288 const rootLines : string [ ] = [ ] ;
314289 const mediaBlocks : string [ ] = [ ] ;
0 commit comments