figmascopeのデフォルトエクスポートターゲットはJetpack Composeです。これは恣意的ではありません—ComposeのレイアウトモデルColumn、Row、Box、ModifierはIRノードの種類(スタック、オーバーレイ、アブソリュート、リーフ)に密接にマッピングされます。FigmaのVerticalスタックはComposeのColumnです。変換は機械的であり、エージェント駆動のコードジェンに適しています。
このウォークスルーではIR→Composeのマッピングを詳しく説明し、対応するComposableを持つ実際のJSONフラグメントを示し、トークンマッピング層を説明します。
Composeがデフォルトターゲットである理由
3つの構造的な理由:
- オートレイアウト ↔ Column/Row。FigmaのオートレイアウトフレームEDEK(現代のFigmaデザインの大部分)は
kind: "stack"ノードとしてエクスポートされます。スタックノードにはaxisプロパティがあります—verticalはColumnに、horizontalはRowにマッピングされます。これは解釈ステップなしの1:1マッピングです。 - スペーシングトークン ↔ dp値。Composeはすべてのレイアウト寸法に
Dpを使います。tokens.jsonのトークン値は単位なし整数(例:spacing.16 = 16)で、16.dpに直接マッピングされます。変換も、スケーリングも不要です。 - カラートークン ↔ Colorコンポーザブル。
tokens.jsonのFigmaの16進値は一つの変換でColor(0xFFrrggbb)にマッピングされます。トークンキーはテーマ内のセマンティック変数名になります。
IRノードの種類とComposeマッピング
| IR種類 | プロパティ | Composeプリミティブ |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | レイヤーされた子 | Box |
absolute | x、y、width、height | Box with Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text with TextStyle |
leaf | type: "rectangle" with fill | Box(Modifier.background(Color(...))) |
すべてのノード種類はspacingプロパティ(子間のギャップ)とpaddingオブジェクト(上/右/下/左)を持てます。両方ともトークンキーを参照します。
トークンマッピングの詳細
tokens.jsonファイルはこのようになっています:
{
"spacing": {
"4": 4, "8": 8, "12": 12, "16": 16,
"20": 20, "24": 24, "32": 32, "48": 48
},
"radius": {
"4": 4, "8": 8, "12": 12, "16": 16, "full": 9999
},
"color": {
"7f5cfe": "#7F5CFE",
"1a1a2e": "#1A1A2E",
"f6f2ea": "#F6F2EA",
"ffffff": "#FFFFFF",
"e53935": "#E53935"
},
"typography": {
"heading.24": { "size": 24, "weight": 700, "lineHeight": 1.2 },
"body.14": { "size": 14, "weight": 400, "lineHeight": 1.5 },
"label.12": { "size": 12, "weight": 500, "lineHeight": 1.4 }
}
}
マッピングルール:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(完全アルファのために0xFFを先頭に追加)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
トークンキーは意図的に不透明(カラーは16進文字列、スペーシングは数値文字列)で、モデルが特定の命名規則に偏らないようにします。あなたのComposeテーマはIRとは独立してセマンティック名—colorPrimary、spacingMd—にエイリアスできます。
実際の例:ホーム画面JSONからComposableへ
簡略化したホーム画面IRを示します。ヘッダーリーフとカードリストを持つVerticalスタック:
{
"name": "home",
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.24",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"fill": "color.f6f2ea",
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.title",
"typography": "typography.heading.24",
"fill": "color.1a1a2e"
},
{
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.12",
"children": [
{
"kind": "overlay",
"radius": "radius.12",
"fill": "color.ffffff",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.card.label",
"typography": "typography.label.12",
"fill": "color.7f5cfe"
}
]
}
]
}
]
}
このIRからエージェントが生成すべき対応するComposable:
@Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF6F2EA))
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = stringResource(R.string.home_title),
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 28.8.sp,
color = Color(0xFF1A1A2E)
)
)
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFFFFFFFF))
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_card_label),
style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
lineHeight = 16.8.sp,
color = Color(0xFF7F5CFE)
)
)
}
}
}
}
Composable内のすべての値はIRのトークンキーに追跡されます。何もハードコードされていません—16.dpはspacing.16から、24.dpはspacing.24から、Color(0xFF7F5CFE)はcolor.7f5cfeから来ています。
文字列ref — stringResourceマッピング
IR内のすべてのテキストノードはドット記法キーを持つstringRefを持ちます。strings.jsonファイルはキーを表示値とフォールバックにマッピングします:
{
"home.title": { "value": "Good morning", "fallback": "Good morning" },
"home.card.label": { "value": "Today's summary", "fallback": "Summary" }
}
ドット記法はAndroid文字列リソースIDにドットをアンダースコアに置き換えてマッピングされます:home.title → R.string.home_title。fallbackフィールドはstrings.xmlにリソースがまだ存在しない場合にリテラル文字列としてハードコードするものです:
text = stringResource(R.string.home_title, "Good morning")
エージェントには常にフォールバックフィールドを使うよう指示してください—空文字列ではなく—そうするとstrings.xmlが追加される前の開発中も画面が読めます。
アブソリュートポジショニング
kind: "absolute"のノードはFigmaの座標を直接使います。これらは要素が重なり合っているデザインや特定の位置にアンカーされた要素に現れます。ComposeのマッピングはParentとしてBoxを使い、子にModifier.offsetを使います:
// IR: { "kind": "absolute", "x": 24, "y": 80, "width": 120, "height": 40 }
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.offset(x = 24.dp, y = 80.dp)
.size(width = 120.dp, height = 40.dp)
) {
// children
}
}
アブソリュートポジショニングは構造化されたFigmaファイル(ほとんどの現代ファイルはオートレイアウトを使う)では一般的ではありません。見た場合は、デザイン意図が本当にアブソリュートかどうか、それともデザイナーがオートレイアウト制約を適用しなかっただけかを確認してください—IRは意図を推測できません。
正直なギャップ
バンドルは一般的なケースをうまくカバーします。いくつかカバーしない点があります:
- グラデーションフィル。グラデーションに遭遇するとIRは警告をエクスポートします。ノードの背景フィルは省略されます。
// TODO: gradientを残して手動で実装してください。 - 複雑なエフェクト。ドロップシャドウとぼかしはIRで表現されません。存在する場合は
_meta.jsonの警告に現れます。 - ベクターアイコン。IRはアイコンノードのパスデータではなく参照IDを保存します。アイコンを実際のdrawableまたはComposeアイコンに別途解決する必要があります。
- ネストされたコンポーネント。IRはインスタンスノードに
componentIdを含みます。components/inventory.jsonはIDを名前にマッピングします。コンポーネントを個別に実装し、Parent ComposableからEDEK名で参照してください。
これらのギャップは明示的です—_meta.jsonの警告とCONTEXT.mdに現れます。エージェントはそれらを静かに推測しません。
figmascopeのメインアプリからコンテキストバンドルをエクスポートして、Claude CodeまたはCursorと一緒に使って、IRから直接Composableを実装してください。スクリーンショットの推測なし、ハードコードされた値なし。