Skip to content

Commit f1a44e4

Browse files
committed
Implement responsive
1 parent 525662c commit f1a44e4

File tree

5 files changed

+709
-2
lines changed

5 files changed

+709
-2
lines changed

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npm run build:*)",
5+
"Bash(cmd /c \"npm run build 2>&1\")",
6+
"Bash(bun run build:*)",
7+
"Bash(bun run tsc:*)"
8+
]
9+
}
10+
}

src/code-impl.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,45 @@
11
import { Codegen } from './codegen/Codegen'
2+
import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen'
23
import { exportDevup, importDevup } from './commands/devup'
34
import { exportAssets } from './commands/exportAssets'
45
import { exportComponents } from './commands/exportComponents'
56

67
export function registerCodegen(ctx: typeof figma) {
78
if (ctx.editorType === 'dev' && ctx.mode === 'codegen') {
8-
ctx.codegen.on('generate', async ({ node, language, ...rest }) => {
9-
console.info(rest, node)
9+
ctx.codegen.on('generate', async ({ node, language }) => {
1010
switch (language) {
1111
case 'devup-ui': {
1212
const time = Date.now()
1313
const codegen = new Codegen(node)
1414
await codegen.run()
1515
const componentsCodes = codegen.getComponentsCodes()
1616
console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`)
17+
18+
// 반응형 코드 생성 (부모가 Section인 경우)
19+
const parentSection = ResponsiveCodegen.hasParentSection(node)
20+
let responsiveResult: {
21+
title: string
22+
language: 'TYPESCRIPT'
23+
code: string
24+
}[] = []
25+
26+
if (parentSection) {
27+
try {
28+
const responsiveCodegen = new ResponsiveCodegen(parentSection)
29+
const responsiveCode =
30+
await responsiveCodegen.generateResponsiveCode()
31+
responsiveResult = [
32+
{
33+
title: `${parentSection.name} - Responsive`,
34+
language: 'TYPESCRIPT' as const,
35+
code: responsiveCode,
36+
},
37+
]
38+
} catch (e) {
39+
console.error('[responsive] Error generating responsive code:', e)
40+
}
41+
}
42+
1743
return [
1844
...(node.type === 'COMPONENT' ||
1945
node.type === 'COMPONENT_SET' ||
@@ -45,6 +71,7 @@ export function registerCodegen(ctx: typeof figma) {
4571
},
4672
] as const)
4773
: []),
74+
...responsiveResult,
4875
]
4976
}
5077
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { getProps } from '../props'
2+
import { renderNode } from '../render'
3+
import { getDevupComponentByNode } from '../utils/get-devup-component'
4+
import {
5+
BREAKPOINT_ORDER,
6+
type BreakpointKey,
7+
getBreakpointByWidth,
8+
mergePropsToResponsive,
9+
optimizeResponsiveValue,
10+
} from './index'
11+
12+
type PropValue = boolean | string | number | undefined | null | object
13+
type Props = Record<string, PropValue>
14+
15+
interface NodePropsMap {
16+
breakpoint: BreakpointKey
17+
props: Props
18+
children: Map<string, NodePropsMap[]>
19+
nodeType: string
20+
nodeName: string
21+
node: SceneNode
22+
}
23+
24+
/**
25+
* Generate responsive code by merging children inside a Section.
26+
*/
27+
export class ResponsiveCodegen {
28+
private breakpointNodes: Map<BreakpointKey, SceneNode> = new Map()
29+
30+
constructor(private sectionNode: SectionNode) {
31+
this.categorizeChildren()
32+
}
33+
34+
/**
35+
* Group Section children by width to decide breakpoints.
36+
*/
37+
private categorizeChildren() {
38+
for (const child of this.sectionNode.children) {
39+
if ('width' in child) {
40+
const breakpoint = getBreakpointByWidth(child.width)
41+
// If multiple nodes share a breakpoint, keep the first.
42+
if (!this.breakpointNodes.has(breakpoint)) {
43+
this.breakpointNodes.set(breakpoint, child)
44+
}
45+
}
46+
}
47+
}
48+
49+
/**
50+
* Recursively extract props and children from a node.
51+
* Reuses getProps.
52+
*/
53+
private async extractNodeProps(
54+
node: SceneNode,
55+
breakpoint: BreakpointKey,
56+
): Promise<NodePropsMap> {
57+
const props = await getProps(node)
58+
const children = new Map<string, NodePropsMap[]>()
59+
60+
if ('children' in node) {
61+
for (const child of node.children) {
62+
const childProps = await this.extractNodeProps(child, breakpoint)
63+
const existing = children.get(child.name) || []
64+
existing.push(childProps)
65+
children.set(child.name, existing)
66+
}
67+
}
68+
69+
return {
70+
breakpoint,
71+
props,
72+
children,
73+
nodeType: node.type,
74+
nodeName: node.name,
75+
node,
76+
}
77+
}
78+
79+
/**
80+
* Generate responsive code.
81+
*/
82+
async generateResponsiveCode(): Promise<string> {
83+
if (this.breakpointNodes.size === 0) {
84+
return '// No responsive variants found in section'
85+
}
86+
87+
if (this.breakpointNodes.size === 1) {
88+
// If only one breakpoint, generate normal code (reuse existing path).
89+
const [, node] = [...this.breakpointNodes.entries()][0]
90+
return await this.generateNodeCode(node, 0)
91+
}
92+
93+
// Extract props per breakpoint node.
94+
const breakpointNodeProps = new Map<BreakpointKey, NodePropsMap>()
95+
for (const [bp, node] of this.breakpointNodes) {
96+
const nodeProps = await this.extractNodeProps(node, bp)
97+
breakpointNodeProps.set(bp, nodeProps)
98+
}
99+
100+
// Merge responsively and generate code.
101+
return await this.generateMergedCode(breakpointNodeProps, 0)
102+
}
103+
104+
/**
105+
* Generate merged responsive code.
106+
* Reuses renderNode.
107+
*/
108+
private async generateMergedCode(
109+
nodesByBreakpoint: Map<BreakpointKey, NodePropsMap>,
110+
depth: number,
111+
): Promise<string> {
112+
// Merge props.
113+
const propsMap = new Map<BreakpointKey, Props>()
114+
for (const [bp, nodeProps] of nodesByBreakpoint) {
115+
propsMap.set(bp, nodeProps.props)
116+
}
117+
const mergedProps = mergePropsToResponsive(propsMap)
118+
119+
// Decide component type from the first node (reuse existing util).
120+
const firstNodeProps = [...nodesByBreakpoint.values()][0]
121+
const component = getDevupComponentByNode(
122+
firstNodeProps.node,
123+
firstNodeProps.props,
124+
)
125+
126+
// Merge child nodes (preserve order).
127+
const childrenCodes: string[] = []
128+
const processedChildNames = new Set<string>()
129+
130+
// Base order on the first breakpoint children.
131+
const firstBreakpointChildren = firstNodeProps.children
132+
const allChildNames: string[] = []
133+
134+
// Keep the first breakpoint child order.
135+
for (const name of firstBreakpointChildren.keys()) {
136+
allChildNames.push(name)
137+
processedChildNames.add(name)
138+
}
139+
140+
// Add children that exist only in other breakpoints.
141+
for (const nodeProps of nodesByBreakpoint.values()) {
142+
for (const name of nodeProps.children.keys()) {
143+
if (!processedChildNames.has(name)) {
144+
allChildNames.push(name)
145+
processedChildNames.add(name)
146+
}
147+
}
148+
}
149+
150+
for (const childName of allChildNames) {
151+
const childByBreakpoint = new Map<BreakpointKey, NodePropsMap>()
152+
const presentBreakpoints = new Set<BreakpointKey>()
153+
154+
for (const [bp, nodeProps] of nodesByBreakpoint) {
155+
const children = nodeProps.children.get(childName)
156+
if (children && children.length > 0) {
157+
childByBreakpoint.set(bp, children[0])
158+
presentBreakpoints.add(bp)
159+
}
160+
}
161+
162+
if (childByBreakpoint.size > 0) {
163+
// Add display props when a child exists only at specific breakpoints.
164+
if (presentBreakpoints.size < nodesByBreakpoint.size) {
165+
const displayProps = this.getDisplayProps(
166+
presentBreakpoints,
167+
new Set(nodesByBreakpoint.keys()),
168+
)
169+
for (const nodeProps of childByBreakpoint.values()) {
170+
Object.assign(nodeProps.props, displayProps)
171+
}
172+
}
173+
174+
const childCode = await this.generateMergedCode(
175+
childByBreakpoint,
176+
depth,
177+
)
178+
childrenCodes.push(childCode)
179+
}
180+
}
181+
182+
// Reuse renderNode.
183+
return renderNode(component, mergedProps, depth, childrenCodes)
184+
}
185+
186+
/**
187+
* Build display props so a child shows only on present breakpoints.
188+
*/
189+
private getDisplayProps(
190+
presentBreakpoints: Set<BreakpointKey>,
191+
allBreakpoints: Set<BreakpointKey>,
192+
): Props {
193+
// Always use 5 slots: [mobile, sm, tablet, lg, pc]
194+
// If the child exists on the breakpoint: null (visible); otherwise 'none' (hidden).
195+
// If the Section lacks that breakpoint entirely: null.
196+
const displayValues: (string | null)[] = BREAKPOINT_ORDER.map((bp) => {
197+
if (!allBreakpoints.has(bp)) return null // Section lacks this breakpoint
198+
return presentBreakpoints.has(bp) ? null : 'none'
199+
})
200+
201+
// If all null, return empty object.
202+
if (displayValues.every((v) => v === null)) {
203+
return {}
204+
}
205+
206+
// Remove trailing nulls only (keep leading nulls).
207+
while (
208+
displayValues.length > 0 &&
209+
displayValues[displayValues.length - 1] === null
210+
) {
211+
displayValues.pop()
212+
}
213+
214+
// Empty array => empty object.
215+
if (displayValues.length === 0) {
216+
return {}
217+
}
218+
219+
return { display: optimizeResponsiveValue(displayValues) }
220+
}
221+
222+
/**
223+
* Generate code for a single node (fallback).
224+
* Reuses existing module.
225+
*/
226+
private async generateNodeCode(
227+
node: SceneNode,
228+
depth: number,
229+
): Promise<string> {
230+
const props = await getProps(node)
231+
const childrenCodes: string[] = []
232+
233+
if ('children' in node) {
234+
for (const child of node.children) {
235+
const childCode = await this.generateNodeCode(child, depth + 1)
236+
childrenCodes.push(childCode)
237+
}
238+
}
239+
240+
const component = getDevupComponentByNode(node, props)
241+
return renderNode(component, props, depth, childrenCodes)
242+
}
243+
244+
/**
245+
* Check if node is Section and can generate responsive.
246+
*/
247+
static canGenerateResponsive(node: SceneNode): node is SectionNode {
248+
return node.type === 'SECTION'
249+
}
250+
251+
/**
252+
* Return parent Section if exists.
253+
*/
254+
static hasParentSection(node: SceneNode): SectionNode | null {
255+
if (node.parent?.type === 'SECTION') {
256+
return node.parent as SectionNode
257+
}
258+
return null
259+
}
260+
}

0 commit comments

Comments
 (0)