@@ -51,8 +51,8 @@ export class ModelHandler implements ModelSpec {
5151 // ── Phase 1: Resolve primitive tokens ──────────────────────────
5252 // Colors
5353 if ( input . colors ) {
54- for ( const [ name , raw ] of Object . entries ( input . colors ) ) {
55- if ( isTokenReference ( raw ) ) {
54+ forEachLeaf ( input . colors , ( name , raw ) => {
55+ if ( typeof raw === 'string' && isTokenReference ( raw ) ) {
5656 // Store raw reference for later resolution
5757 symbolTable . set ( `colors.${ name } ` , raw ) ;
5858 } else if ( isValidColor ( raw ) ) {
@@ -68,7 +68,7 @@ export class ModelHandler implements ModelSpec {
6868 // Store as-is for fallback
6969 symbolTable . set ( `colors.${ name } ` , raw ) ;
7070 }
71- }
71+ } ) ;
7272 }
7373
7474 // Typography
@@ -82,7 +82,7 @@ export class ModelHandler implements ModelSpec {
8282
8383 // Rounded
8484 if ( input . rounded ) {
85- for ( const [ name , raw ] of Object . entries ( input . rounded ) ) {
85+ forEachLeaf ( input . rounded , ( name , raw ) => {
8686 if ( typeof raw === 'string' ) {
8787 if ( isParseableDimension ( raw ) ) {
8888 const resolved = parseDimension ( raw ) ;
@@ -106,39 +106,39 @@ export class ModelHandler implements ModelSpec {
106106 symbolTable . set ( `rounded.${ name } ` , raw ) ;
107107 }
108108 }
109- }
109+ } ) ;
110110 }
111111
112112 // Spacing
113113 if ( input . spacing ) {
114- for ( const [ name , raw ] of Object . entries ( input . spacing ) ) {
114+ forEachLeaf ( input . spacing , ( name , raw ) => {
115115 if ( isParseableDimension ( raw ) ) {
116116 const resolved = parseDimension ( raw ) ;
117117 spacing . set ( name , resolved ) ;
118118 symbolTable . set ( `spacing.${ name } ` , resolved ) ;
119119 } else {
120120 symbolTable . set ( `spacing.${ name } ` , raw ) ;
121121 }
122- }
122+ } ) ;
123123 }
124124
125125 // ── Phase 2: Resolve chained color references ──────────────────
126126 // Iterate color entries that are still raw references and resolve them
127127 if ( input . colors ) {
128- for ( const [ name , raw ] of Object . entries ( input . colors ) ) {
129- if ( isTokenReference ( raw ) ) {
128+ forEachLeaf ( input . colors , ( name , raw ) => {
129+ if ( typeof raw === 'string' && isTokenReference ( raw ) ) {
130130 const resolved = resolveReference ( symbolTable , raw . slice ( 1 , - 1 ) , new Set ( ) ) ;
131131 if ( resolved !== null && typeof resolved === 'object' && 'type' in resolved && resolved . type === 'color' ) {
132132 colors . set ( name , resolved as ResolvedColor ) ;
133133 symbolTable . set ( `colors.${ name } ` , resolved ) ;
134134 }
135135 }
136- }
136+ } ) ;
137137 }
138138
139139 // Resolve chained rounded references
140140 if ( input . rounded ) {
141- for ( const [ name , raw ] of Object . entries ( input . rounded ) ) {
141+ forEachLeaf ( input . rounded , ( name , raw ) => {
142142 if ( typeof raw === 'string' && isTokenReference ( raw ) ) {
143143 const resolved = resolveReference ( symbolTable , raw . slice ( 1 , - 1 ) , new Set ( ) ) ;
144144 if (
@@ -151,12 +151,12 @@ export class ModelHandler implements ModelSpec {
151151 symbolTable . set ( `rounded.${ name } ` , resolved ) ;
152152 }
153153 }
154- }
154+ } ) ;
155155 }
156156
157157 // Resolve chained spacing references
158158 if ( input . spacing ) {
159- for ( const [ name , raw ] of Object . entries ( input . spacing ) ) {
159+ forEachLeaf ( input . spacing , ( name , raw ) => {
160160 if ( typeof raw === 'string' && isTokenReference ( raw ) ) {
161161 const resolved = resolveReference ( symbolTable , raw . slice ( 1 , - 1 ) , new Set ( ) ) ;
162162 if (
@@ -169,7 +169,7 @@ export class ModelHandler implements ModelSpec {
169169 symbolTable . set ( `spacing.${ name } ` , resolved ) ;
170170 }
171171 }
172- }
172+ } ) ;
173173 }
174174
175175 // ── Phase 3: Build components ──────────────────────────────────
@@ -390,4 +390,19 @@ export function contrastRatio(a: ResolvedColor, b: ResolvedColor): number {
390390 const L1 = Math . max ( a . luminance , b . luminance ) ;
391391 const L2 = Math . min ( a . luminance , b . luminance ) ;
392392 return ( L1 + 0.05 ) / ( L2 + 0.05 ) ;
393+ }
394+
395+ /**
396+ * Recursively iterate over an object and call a function for each leaf node.
397+ * Leaf node paths are dot-separated (e.g. "background.light").
398+ */
399+ function forEachLeaf ( obj : Record < string , any > , fn : ( path : string , value : any ) => void , prefix = '' ) {
400+ for ( const [ key , value ] of Object . entries ( obj ) ) {
401+ const fullPath = prefix ? `${ prefix } .${ key } ` : key ;
402+ if ( value !== null && typeof value === 'object' && ! Array . isArray ( value ) ) {
403+ forEachLeaf ( value , fn , fullPath ) ;
404+ } else {
405+ fn ( fullPath , value ) ;
406+ }
407+ }
393408}
0 commit comments