@@ -134,7 +134,7 @@ export function generateWebsiteArtifact(
134134 } ;
135135}
136136
137- function renderNode ( node : FigmaSceneNode , context : RenderContext , isRoot = false ) : string {
137+ function renderNode ( node : FigmaSceneNode , context : RenderContext , isRoot = false , parent ?: FigmaSceneNode ) : string {
138138 context . nodeCount += 1 ;
139139
140140 if ( node . visible === false ) {
@@ -152,22 +152,22 @@ function renderNode(node: FigmaSceneNode, context: RenderContext, isRoot = false
152152 }
153153
154154 if ( node . type === "TEXT" ) {
155- return renderText ( node , context ) ;
155+ return renderText ( node , context , parent ) ;
156156 }
157157
158158 if ( node . type === "IMAGE" ) {
159- return renderImage ( node , context ) ;
159+ return renderImage ( node , context , parent ) ;
160160 }
161161
162162 if ( node . type === "RECTANGLE" && ! node . children ?. length ) {
163- return renderRectangle ( node , context ) ;
163+ return renderRectangle ( node , context , parent ) ;
164164 }
165165
166- return renderContainer ( node , context , isRoot ) ;
166+ return renderContainer ( node , context , isRoot , parent ) ;
167167}
168168
169- function renderContainer ( node : FigmaSceneNode , context : RenderContext , isRoot : boolean ) : string {
170- const className = addCssRule ( node , context , [ "section" ] ) ;
169+ function renderContainer ( node : FigmaSceneNode , context : RenderContext , isRoot : boolean , parent ?: FigmaSceneNode ) : string {
170+ const className = addCssRule ( node , context , [ "section" ] , isRoot , parent ) ;
171171 const tagName = isRoot ? "main" : isButtonLike ( node ) ? "a" : "section" ;
172172 const attrs = [
173173 `class="${ className } "` ,
@@ -178,21 +178,21 @@ function renderContainer(node: FigmaSceneNode, context: RenderContext, isRoot: b
178178 return `<${ tagName } ${ attrs . join ( " " ) } >\n${ children } \n</${ tagName } >` ;
179179}
180180
181- function renderText ( node : FigmaSceneNode , context : RenderContext ) : string {
182- const className = addCssRule ( node , context , [ "text" ] ) ;
181+ function renderText ( node : FigmaSceneNode , context : RenderContext , parent ?: FigmaSceneNode ) : string {
182+ const className = addCssRule ( node , context , [ "text" ] , false , parent ) ;
183183 const content = escapeHtml ( node . characters || "" ) ;
184184 const tagName = textTagName ( node ) ;
185185
186186 return `<${ tagName } class="${ className } ">${ content } </${ tagName } >` ;
187187}
188188
189- function renderRectangle ( node : FigmaSceneNode , context : RenderContext ) : string {
190- const className = addCssRule ( node , context , [ "shape" ] ) ;
189+ function renderRectangle ( node : FigmaSceneNode , context : RenderContext , parent ?: FigmaSceneNode ) : string {
190+ const className = addCssRule ( node , context , [ "shape" ] , false , parent ) ;
191191 return `<div class="${ className } " aria-hidden="true"></div>` ;
192192}
193193
194- function renderImage ( node : FigmaSceneNode , context : RenderContext ) : string {
195- const className = addCssRule ( node , context , [ "image" ] ) ;
194+ function renderImage ( node : FigmaSceneNode , context : RenderContext , parent ?: FigmaSceneNode ) : string {
195+ const className = addCssRule ( node , context , [ "image" ] , false , parent ) ;
196196 const src = resolveImageSource ( node , context ) ;
197197 const alt = escapeAttribute ( node . image ?. alt || node . name || "" ) ;
198198
@@ -201,17 +201,17 @@ function renderImage(node: FigmaSceneNode, context: RenderContext): string {
201201
202202function renderChildren ( node : FigmaSceneNode , context : RenderContext ) : string {
203203 return ( node . children || [ ] )
204- . map ( ( child ) => renderNode ( child , context ) )
204+ . map ( ( child ) => renderNode ( child , context , false , node ) )
205205 . filter ( Boolean )
206206 . join ( "\n" ) ;
207207}
208208
209- function addCssRule ( node : FigmaSceneNode , context : RenderContext , parts : string [ ] ) : string {
209+ function addCssRule ( node : FigmaSceneNode , context : RenderContext , parts : string [ ] , isRoot = false , parent ?: FigmaSceneNode ) : string {
210210 const baseClass = toClassName ( [ node . name ] . concat ( parts ) . join ( "-" ) ) ;
211211 const count = context . usedClasses . get ( baseClass ) || 0 ;
212212 context . usedClasses . set ( baseClass , count + 1 ) ;
213213 const className = count === 0 ? baseClass : `${ baseClass } -${ count + 1 } ` ;
214- const declarations = cssDeclarations ( node ) ;
214+ const declarations = cssDeclarations ( node , isRoot , parent ) ;
215215
216216 if ( declarations . length ) {
217217 context . cssRules . push ( `.${ className } {\n${ declarations . map ( ( rule ) => ` ${ rule } ` ) . join ( "\n" ) } \n}` ) ;
@@ -220,28 +220,43 @@ function addCssRule(node: FigmaSceneNode, context: RenderContext, parts: string[
220220 return className ;
221221}
222222
223- function cssDeclarations ( node : FigmaSceneNode ) : string [ ] {
223+ function cssDeclarations ( node : FigmaSceneNode , isRoot : boolean , parent ?: FigmaSceneNode ) : string [ ] {
224224 const declarations : string [ ] = [ ] ;
225225 const fill = firstVisiblePaint ( node . fills ) ;
226226 const stroke = firstVisiblePaint ( node . strokes ) ;
227+ const positioned = shouldAbsolutelyPosition ( node , parent ) ;
227228
229+ if ( positioned ) {
230+ declarations . push ( "position: absolute;" ) ;
231+ declarations . push ( `left: ${ formatPx ( ( node . x || 0 ) - ( parent ?. x || 0 ) ) } ;` ) ;
232+ declarations . push ( `top: ${ formatPx ( ( node . y || 0 ) - ( parent ?. y || 0 ) ) } ;` ) ;
233+ }
228234 if ( node . width !== undefined ) declarations . push ( `width: ${ formatPx ( node . width ) } ;` ) ;
229235 if ( node . height !== undefined && node . type !== "TEXT" ) declarations . push ( `min-height: ${ formatPx ( node . height ) } ;` ) ;
230236 if ( node . opacity !== undefined && node . opacity < 1 ) declarations . push ( `opacity: ${ node . opacity } ;` ) ;
231237 if ( node . cornerRadius !== undefined ) declarations . push ( `border-radius: ${ formatPx ( node . cornerRadius ) } ;` ) ;
232- if ( fill ) declarations . push ( `background: ${ paintToCss ( fill ) } ;` ) ;
238+ if ( fill && node . type === "TEXT" ) declarations . push ( `color: ${ paintToCss ( fill ) } ;` ) ;
239+ if ( fill && node . type !== "TEXT" ) declarations . push ( `background: ${ paintToCss ( fill ) } ;` ) ;
233240 if ( stroke ) declarations . push ( `border: 1px solid ${ paintToCss ( stroke ) } ;` ) ;
234241
235242 if ( containerTypes . has ( node . type ) ) {
236- declarations . push ( "display: flex;" ) ;
237- declarations . push ( "flex-direction: column;" ) ;
238- declarations . push ( "gap: 1rem;" ) ;
243+ if ( ! positioned ) declarations . push ( "position: relative;" ) ;
244+ if ( ! hasPositionedChildren ( node ) ) {
245+ declarations . push ( "display: flex;" ) ;
246+ declarations . push ( "flex-direction: column;" ) ;
247+ declarations . push ( "gap: 1rem;" ) ;
248+ }
249+ if ( isRoot ) {
250+ declarations . push ( "margin: 0 auto;" ) ;
251+ declarations . push ( "overflow: hidden;" ) ;
252+ }
239253 }
240254
241255 if ( node . type === "TEXT" ) {
256+ declarations . push ( "margin: 0;" ) ;
242257 if ( node . style ?. fontFamily ) declarations . push ( `font-family: ${ quoteFontFamily ( node . style . fontFamily ) } ;` ) ;
243258 if ( node . style ?. fontSize ) declarations . push ( `font-size: ${ formatPx ( node . style . fontSize ) } ;` ) ;
244- if ( node . style ?. fontWeight ) declarations . push ( `font-weight: ${ node . style . fontWeight } ;` ) ;
259+ if ( node . style ?. fontWeight ) declarations . push ( `font-weight: ${ formatFontWeight ( node . style . fontWeight ) } ;` ) ;
245260 if ( node . style ?. lineHeight ) declarations . push ( `line-height: ${ formatLineHeight ( node . style . lineHeight ) } ;` ) ;
246261 if ( node . style ?. textAlignHorizontal ) declarations . push ( `text-align: ${ textAlign ( node . style . textAlignHorizontal ) } ;` ) ;
247262 }
@@ -260,6 +275,14 @@ function cssDeclarations(node: FigmaSceneNode): string[] {
260275 return declarations ;
261276}
262277
278+ function shouldAbsolutelyPosition ( node : FigmaSceneNode , parent ?: FigmaSceneNode ) : boolean {
279+ return ! ! parent && node . x !== undefined && node . y !== undefined && hasPositionedChildren ( parent ) ;
280+ }
281+
282+ function hasPositionedChildren ( node : FigmaSceneNode ) : boolean {
283+ return ( node . children || [ ] ) . some ( ( child ) => child . x !== undefined && child . y !== undefined ) ;
284+ }
285+
263286function resolveImageSource ( node : FigmaSceneNode , context : RenderContext ) : string {
264287 if ( node . image ?. src ) {
265288 return node . image . src ;
@@ -377,6 +400,25 @@ function formatLineHeight(value: number | string): string {
377400 return typeof value === "number" ? formatPx ( value ) : value ;
378401}
379402
403+ function formatFontWeight ( value : number | string ) : string {
404+ if ( typeof value === "number" ) {
405+ return String ( value ) ;
406+ }
407+
408+ const normalized = value . toLowerCase ( ) ;
409+ if ( normalized . includes ( "thin" ) ) return "100" ;
410+ if ( normalized . includes ( "extra light" ) || normalized . includes ( "ultra light" ) ) return "200" ;
411+ if ( normalized . includes ( "light" ) ) return "300" ;
412+ if ( normalized . includes ( "regular" ) || normalized . includes ( "book" ) ) return "400" ;
413+ if ( normalized . includes ( "medium" ) ) return "500" ;
414+ if ( normalized . includes ( "semi bold" ) || normalized . includes ( "semibold" ) || normalized . includes ( "demi bold" ) ) return "600" ;
415+ if ( normalized . includes ( "extra bold" ) || normalized . includes ( "ultra bold" ) ) return "800" ;
416+ if ( normalized . includes ( "bold" ) ) return "700" ;
417+ if ( normalized . includes ( "black" ) || normalized . includes ( "heavy" ) ) return "900" ;
418+
419+ return value ;
420+ }
421+
380422function textAlign ( value : string ) : string {
381423 const normalized = value . toLowerCase ( ) ;
382424 return normalized === "justified" ? "justify" : normalized ;
0 commit comments