2026-02-17 13:27:09 +01:00
#!/usr/bin/env node
/ * *
* Design Diff Analyzer
* Analyzes changes to ui - design . pen and generates implementation tasks for Claude Code
* /
2026-02-17 14:08:04 +01:00
import { execSync } from 'child_process' ;
import fs from 'fs' ;
2026-02-17 13:27:09 +01:00
function getDiff ( ) {
try {
return execSync ( 'git diff HEAD~1 ui-design.pen' , { encoding : 'utf8' } ) ;
} catch ( e ) {
return '' ;
}
}
function analyzeDiff ( diff ) {
const changes = {
colors : [ ] ,
typography : [ ] ,
spacing : [ ] ,
layout : [ ] ,
content : [ ]
} ;
const lines = diff . split ( '\n' ) ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
// Color changes
if ( line . includes ( '"fill":' ) || line . includes ( '"backgroundColor":' ) ) {
const oldMatch = lines [ i - 1 ] ? . match ( /"(fill|backgroundColor)":\s*"([^"]+)"/ ) ;
const newMatch = line . match ( /"(fill|backgroundColor)":\s*"([^"]+)"/ ) ;
if ( oldMatch && newMatch && oldMatch [ 2 ] !== newMatch [ 2 ] ) {
changes . colors . push ( {
type : oldMatch [ 1 ] ,
from : oldMatch [ 2 ] ,
to : newMatch [ 2 ]
} ) ;
}
}
// Font size changes
if ( line . includes ( '"fontSize":' ) ) {
const oldMatch = lines [ i - 1 ] ? . match ( /"fontSize":\s*(\d+)/ ) ;
const newMatch = line . match ( /"fontSize":\s*(\d+)/ ) ;
if ( oldMatch && newMatch && oldMatch [ 1 ] !== newMatch [ 1 ] ) {
changes . typography . push ( {
type : 'fontSize' ,
from : parseInt ( oldMatch [ 1 ] ) ,
to : parseInt ( newMatch [ 1 ] )
} ) ;
}
}
// Spacing/padding changes
if ( line . includes ( '"padding":' ) || line . includes ( '"margin":' ) || line . includes ( '"gap":' ) ) {
const oldMatch = lines [ i - 1 ] ? . match ( /"(padding|margin|gap)":\s*(\d+)/ ) ;
const newMatch = line . match ( /"(padding|margin|gap)":\s*(\d+)/ ) ;
if ( oldMatch && newMatch && oldMatch [ 2 ] !== newMatch [ 2 ] ) {
changes . spacing . push ( {
type : oldMatch [ 1 ] ,
from : parseInt ( oldMatch [ 2 ] ) ,
to : parseInt ( newMatch [ 2 ] )
} ) ;
}
}
// Content changes (text only - no implementation needed)
if ( line . includes ( '"content":' ) ) {
const oldMatch = lines [ i - 1 ] ? . match ( /"content":\s*"([^"]+)"/ ) ;
const newMatch = line . match ( /"content":\s*"([^"]+)"/ ) ;
if ( oldMatch && newMatch && oldMatch [ 1 ] !== newMatch [ 1 ] ) {
changes . content . push ( {
from : oldMatch [ 1 ] ,
to : newMatch [ 1 ]
} ) ;
}
}
2026-02-17 16:36:02 +01:00
// Layout changes - new components added
if ( line . startsWith ( '+' ) && line . includes ( '"name":' ) ) {
// Look back for "type": "frame" in recent lines
const frameTypeMatch = lines . slice ( Math . max ( 0 , i - 5 ) , i + 1 ) . find ( l =>
l . startsWith ( '+' ) && l . includes ( '"type": "frame"' )
) ;
if ( frameTypeMatch ) {
const nameMatch = line . match ( /"name":\s*"([^"]+)"/ ) ;
if ( nameMatch ) {
changes . layout . push ( {
action : 'added' ,
component : nameMatch [ 1 ] ,
type : 'frame'
} ) ;
}
}
}
// Layout changes - components removed
if ( line . startsWith ( '-' ) && line . includes ( '"name":' ) ) {
const frameTypeMatch = lines . slice ( Math . max ( 0 , i - 5 ) , i + 1 ) . find ( l =>
l . startsWith ( '-' ) && l . includes ( '"type": "frame"' )
) ;
if ( frameTypeMatch ) {
const nameMatch = line . match ( /"name":\s*"([^"]+)"/ ) ;
if ( nameMatch ) {
changes . layout . push ( {
action : 'removed' ,
component : nameMatch [ 1 ] ,
type : 'frame'
} ) ;
}
}
}
// Height changes (structural)
if ( line . includes ( '"height":' ) ) {
const oldMatch = lines [ i - 1 ] ? . match ( /"height":\s*(\d+)/ ) ;
const newMatch = line . match ( /"height":\s*(\d+)/ ) ;
if ( oldMatch && newMatch && oldMatch [ 1 ] !== newMatch [ 1 ] ) {
changes . layout . push ( {
action : 'resized' ,
property : 'height' ,
from : parseInt ( oldMatch [ 1 ] ) ,
to : parseInt ( newMatch [ 1 ] )
} ) ;
}
}
2026-02-17 13:27:09 +01:00
}
return changes ;
}
2026-02-17 14:22:31 +01:00
function analyzeIntent ( changes ) {
const intent = {
patterns : [ ] ,
systems : [ ] ,
recommendations : [ ]
} ;
// Detect color-coding system for types
const accentColorIntro = changes . colors . filter ( c =>
c . to . includes ( 'accent-' ) && ! c . from . includes ( 'accent-' )
) ;
if ( accentColorIntro . length >= 4 ) {
const colors = [ ... new Set ( accentColorIntro . map ( c => c . to ) ) ] ;
intent . systems . push ( {
type : 'color-coding' ,
description : 'Type-based color system' ,
colors : colors ,
explanation : 'Each entity type (Project, Procedure, Person, Topic) has its own accent color. This color should be used consistently for: icons, active states, badges, and any UI element representing that type.' ,
critical : '⚠️ IMPORTANT: Active states must use the color **dynamically** based on the note type, not hardcoded. Example: if a Project is selected, use accent-red; if a Procedure is selected, use accent-purple.'
} ) ;
}
// Detect contrast improvements
const contrastChanges = changes . colors . filter ( c =>
c . from === '$--muted-foreground' && c . to === '$--foreground'
) ;
if ( contrastChanges . length > 3 ) {
intent . patterns . push ( {
type : 'contrast-improvement' ,
description : 'Increased text contrast for better readability' ,
count : contrastChanges . length
} ) ;
}
// Detect spacing improvements
if ( changes . spacing . length > 0 ) {
const avgIncrease = changes . spacing . reduce ( ( sum , s ) => sum + ( s . to - s . from ) , 0 ) / changes . spacing . length ;
if ( avgIncrease > 0 ) {
intent . patterns . push ( {
type : 'spacing-increase' ,
description : 'Increased spacing for less cluttered UI' ,
avgIncrease : Math . round ( avgIncrease )
} ) ;
}
}
return intent ;
}
function generateTasks ( changes , intent ) {
2026-02-17 13:27:09 +01:00
const tasks = [ ] ;
// Color changes → update theme.json or CSS variables
if ( changes . colors . length > 0 ) {
const colorList = changes . colors . map ( c =>
` - ${ c . type } : ${ c . from } → ${ c . to } `
) . join ( '\n' ) ;
2026-02-17 14:22:31 +01:00
let details = ` Color changes detected: \n ${ colorList } \n \n Update src/theme.json or CSS variables to match the design. ` ;
// Add intent explanation if color-coding system detected
const colorSystem = intent . systems . find ( s => s . type === 'color-coding' ) ;
if ( colorSystem ) {
details += ` \n \n ${ colorSystem . explanation } \n \n ${ colorSystem . critical } ` ;
}
2026-02-17 13:27:09 +01:00
tasks . push ( {
priority : 'high' ,
description : 'Update color palette' ,
2026-02-17 14:22:31 +01:00
details : details
2026-02-17 13:27:09 +01:00
} ) ;
}
// Typography changes → update theme.json
if ( changes . typography . length > 0 ) {
const typoList = changes . typography . map ( t =>
` - ${ t . type } : ${ t . from } px → ${ t . to } px `
) . join ( '\n' ) ;
tasks . push ( {
priority : 'medium' ,
description : 'Update typography' ,
details : ` Typography changes detected: \n ${ typoList } \n \n Update src/theme.json typography settings. `
} ) ;
}
// Spacing changes → update theme.json
if ( changes . spacing . length > 0 ) {
const spacingList = changes . spacing . map ( s =>
` - ${ s . type } : ${ s . from } px → ${ s . to } px `
) . join ( '\n' ) ;
tasks . push ( {
priority : 'medium' ,
description : 'Update spacing' ,
details : ` Spacing changes detected: \n ${ spacingList } \n \n Update src/theme.json spacing values. `
} ) ;
}
2026-02-17 16:36:02 +01:00
// Layout changes → structural modifications
if ( changes . layout . length > 0 ) {
const added = changes . layout . filter ( l => l . action === 'added' ) ;
const removed = changes . layout . filter ( l => l . action === 'removed' ) ;
const resized = changes . layout . filter ( l => l . action === 'resized' ) ;
let layoutDetails = 'Layout changes detected:\n\n' ;
if ( added . length > 0 ) {
layoutDetails += 'Components added:\n' ;
added . forEach ( c => layoutDetails += ` + ${ c . component } ( ${ c . type } ) \n ` ) ;
layoutDetails += '\n' ;
}
if ( removed . length > 0 ) {
layoutDetails += 'Components removed:\n' ;
removed . forEach ( c => layoutDetails += ` - ${ c . component } ( ${ c . type } ) \n ` ) ;
layoutDetails += '\n' ;
}
if ( resized . length > 0 ) {
layoutDetails += 'Components resized:\n' ;
resized . forEach ( c => layoutDetails += ` • ${ c . property } : ${ c . from } px → ${ c . to } px \n ` ) ;
layoutDetails += '\n' ;
}
layoutDetails += 'Review ui-design.pen diff and update components accordingly.' ;
tasks . push ( {
priority : 'high' ,
description : 'Update layout structure' ,
details : layoutDetails
} ) ;
}
2026-02-17 13:27:09 +01:00
return tasks ;
}
2026-02-17 14:22:31 +01:00
function formatOutput ( tasks , changes , intent ) {
2026-02-17 13:27:09 +01:00
if ( tasks . length === 0 && changes . content . length === 0 ) {
return null ;
}
let output = '🎨 Design changes detected:\n\n' ;
2026-02-17 14:22:31 +01:00
// Add design intent analysis
if ( intent . systems . length > 0 || intent . patterns . length > 0 ) {
output += '🧠 Design Intent Analysis:\n\n' ;
intent . systems . forEach ( system => {
output += ` 📐 ${ system . description } \n ` ;
output += ` ${ system . explanation } \n ` ;
if ( system . critical ) {
output += ` \n ${ system . critical } \n ` ;
}
output += '\n' ;
} ) ;
intent . patterns . forEach ( pattern => {
output += ` • ${ pattern . description } ` ;
if ( pattern . count ) output += ` ( ${ pattern . count } changes) ` ;
if ( pattern . avgIncrease ) output += ` (+ ${ pattern . avgIncrease } px avg) ` ;
output += '\n' ;
} ) ;
output += '\n' ;
}
2026-02-17 13:27:09 +01:00
if ( tasks . length > 0 ) {
output += '📋 Implementation tasks:\n\n' ;
tasks . forEach ( ( task , i ) => {
output += ` ${ i + 1 } . [ ${ task . priority . toUpperCase ( ) } ] ${ task . description } \n ` ;
output += ` ${ task . details } \n \n ` ;
} ) ;
}
if ( changes . content . length > 0 ) {
output += '📝 Content changes (mockup text only, no code changes needed):\n' ;
changes . content . forEach ( c => {
output += ` - " ${ c . from } " → " ${ c . to } " \n ` ;
} ) ;
output += '\n' ;
}
2026-02-17 14:22:31 +01:00
return { output , tasks , intent } ;
2026-02-17 13:27:09 +01:00
}
function main ( ) {
const diff = getDiff ( ) ;
if ( ! diff ) {
console . log ( 'No design changes detected' ) ;
process . exit ( 0 ) ;
}
const changes = analyzeDiff ( diff ) ;
2026-02-17 14:22:31 +01:00
const intent = analyzeIntent ( changes ) ;
const tasks = generateTasks ( changes , intent ) ;
const result = formatOutput ( tasks , changes , intent ) ;
2026-02-17 13:27:09 +01:00
if ( ! result ) {
console . log ( 'No significant design changes detected' ) ;
process . exit ( 0 ) ;
}
console . log ( result . output ) ;
// Output JSON for automation
if ( process . argv . includes ( '--json' ) ) {
console . log ( '\n---JSON---' ) ;
2026-02-17 14:22:31 +01:00
console . log ( JSON . stringify ( { tasks , changes , intent } , null , 2 ) ) ;
2026-02-17 13:27:09 +01:00
}
process . exit ( tasks . length > 0 ? 1 : 0 ) ; // Exit 1 if there are tasks to implement
}
main ( ) ;