Skip to content

Commit 281e625

Browse files
authored
Merge pull request #22 from dev-five-git/fix-selection
Fix selection
2 parents d4f7200 + 7527124 commit 281e625

22 files changed

+26298
-1700
lines changed

src/__tests__/__snapshots__/code.test.ts.snap

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,57 @@ exports[`registerCodegen should register codegen 2`] = `
5353
`;
5454

5555
exports[`registerCodegen should register codegen 3`] = `[]`;
56+
57+
exports[`registerCodegen should generate responsive code when root node is SECTION 1`] = `
58+
[
59+
{
60+
"code":
61+
"<Box boxSize="100%">
62+
<Box boxSize="100%" />
63+
<Box boxSize="100%" />
64+
</Box>"
65+
,
66+
"language": "TYPESCRIPT",
67+
"title": "ResponsiveSection",
68+
},
69+
{
70+
"code":
71+
"import { Box } from '@devup-ui/react'
72+
73+
export function ResponsiveSection() {
74+
return <Box boxSize="100%" />
75+
}"
76+
,
77+
"language": "TYPESCRIPT",
78+
"title": "ResponsiveSection - Responsive",
79+
},
80+
{
81+
"code":
82+
"mkdir -p src/components
83+
84+
echo 'import { Box } from \\'@devup-ui/react\\'
85+
86+
export function ResponsiveSection() {
87+
return <Box boxSize="100%" />
88+
}' > src/components/ResponsiveSection.tsx"
89+
,
90+
"language": "BASH",
91+
"title": "ResponsiveSection - Responsive CLI (Bash)",
92+
},
93+
{
94+
"code":
95+
"New-Item -ItemType Directory -Force -Path src\\components | Out-Null
96+
97+
@'
98+
import { Box } from '@devup-ui/react'
99+
100+
export function ResponsiveSection() {
101+
return <Box boxSize="100%" />
102+
}
103+
'@ | Out-File -FilePath src\\components\\ResponsiveSection.tsx -Encoding UTF8"
104+
,
105+
"language": "BASH",
106+
"title": "ResponsiveSection - Responsive CLI (PowerShell)",
107+
},
108+
]
109+
`;

src/__tests__/code.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,53 @@ describe('registerCodegen', () => {
148148
),
149149
).toMatchSnapshot()
150150
})
151+
152+
it('should generate responsive code when root node is SECTION', async () => {
153+
const figmaMock = {
154+
editorType: 'dev',
155+
mode: 'codegen',
156+
command: 'noop',
157+
codegen: { on: mock(() => {}) },
158+
closePlugin: mock(() => {}),
159+
} as unknown as typeof figma
160+
161+
codeModule.registerCodegen(figmaMock)
162+
163+
const sectionNode = {
164+
type: 'SECTION',
165+
name: 'ResponsiveSection',
166+
visible: true,
167+
children: [
168+
{
169+
type: 'FRAME',
170+
name: 'MobileFrame',
171+
visible: true,
172+
width: 375,
173+
height: 200,
174+
children: [],
175+
layoutMode: 'VERTICAL',
176+
},
177+
{
178+
type: 'FRAME',
179+
name: 'DesktopFrame',
180+
visible: true,
181+
width: 1440,
182+
height: 200,
183+
children: [],
184+
layoutMode: 'HORIZONTAL',
185+
},
186+
],
187+
}
188+
189+
const result = await (
190+
figmaMock.codegen.on as ReturnType<typeof mock>
191+
).mock.calls[0][1]({
192+
node: sectionNode,
193+
language: 'devup-ui',
194+
})
195+
196+
expect(result).toMatchSnapshot()
197+
})
151198
})
152199

153200
it('should not register codegen if figma is not defined', async () => {
@@ -633,4 +680,131 @@ describe('registerCodegen with viewport variant', () => {
633680
)
634681
}
635682
})
683+
684+
it('should generate componentsResponsiveCodes when FRAME contains INSTANCE of COMPONENT_SET with viewport', async () => {
685+
let capturedHandler: CodegenHandler | null = null
686+
687+
const figmaMock = {
688+
editorType: 'dev',
689+
mode: 'codegen',
690+
command: 'noop',
691+
codegen: {
692+
on: (_event: string, handler: CodegenHandler) => {
693+
capturedHandler = handler
694+
},
695+
},
696+
closePlugin: mock(() => {}),
697+
} as unknown as typeof figma
698+
699+
codeModule.registerCodegen(figmaMock)
700+
701+
expect(capturedHandler).not.toBeNull()
702+
if (capturedHandler === null) throw new Error('Handler not captured')
703+
704+
// Create a COMPONENT_SET with viewport variants
705+
const componentSetNode = {
706+
type: 'COMPONENT_SET',
707+
name: 'ResponsiveButton',
708+
visible: true,
709+
componentPropertyDefinitions: {
710+
viewport: {
711+
type: 'VARIANT',
712+
defaultValue: 'desktop',
713+
variantOptions: ['mobile', 'desktop'],
714+
},
715+
},
716+
children: [] as unknown[],
717+
defaultVariant: null as unknown,
718+
}
719+
720+
// Create COMPONENT children for the COMPONENT_SET
721+
const mobileComponent = {
722+
type: 'COMPONENT',
723+
name: 'viewport=mobile',
724+
visible: true,
725+
variantProperties: { viewport: 'mobile' },
726+
children: [],
727+
layoutMode: 'VERTICAL',
728+
width: 320,
729+
height: 100,
730+
parent: componentSetNode,
731+
componentPropertyDefinitions: {},
732+
reactions: [],
733+
}
734+
735+
const desktopComponent = {
736+
type: 'COMPONENT',
737+
name: 'viewport=desktop',
738+
visible: true,
739+
variantProperties: { viewport: 'desktop' },
740+
children: [],
741+
layoutMode: 'HORIZONTAL',
742+
width: 1200,
743+
height: 100,
744+
parent: componentSetNode,
745+
componentPropertyDefinitions: {},
746+
reactions: [],
747+
}
748+
749+
componentSetNode.children = [mobileComponent, desktopComponent]
750+
componentSetNode.defaultVariant = desktopComponent
751+
752+
// Create an INSTANCE that references the desktop component
753+
const instanceNode = {
754+
type: 'INSTANCE',
755+
name: 'ResponsiveButton',
756+
visible: true,
757+
width: 1200,
758+
height: 100,
759+
getMainComponentAsync: async () => desktopComponent,
760+
}
761+
762+
// Create a FRAME that contains the INSTANCE
763+
const frameNode = {
764+
type: 'FRAME',
765+
name: 'MyFrame',
766+
visible: true,
767+
children: [instanceNode],
768+
width: 1400,
769+
height: 200,
770+
layoutMode: 'VERTICAL',
771+
} as unknown as SceneNode
772+
773+
const handler = capturedHandler as CodegenHandler
774+
const result = await handler({
775+
node: frameNode,
776+
language: 'devup-ui',
777+
})
778+
779+
// Should include Components Responsive results
780+
const responsiveResult = result.find(
781+
(r: unknown) =>
782+
typeof r === 'object' &&
783+
r !== null &&
784+
'title' in r &&
785+
(r as { title: string }).title === 'MyFrame - Components Responsive',
786+
)
787+
expect(responsiveResult).toBeDefined()
788+
789+
// Should also include CLI results for Components Responsive
790+
const bashCLI = result.find(
791+
(r: unknown) =>
792+
typeof r === 'object' &&
793+
r !== null &&
794+
'title' in r &&
795+
(r as { title: string }).title ===
796+
'MyFrame - Components Responsive CLI (Bash)',
797+
)
798+
expect(bashCLI).toBeDefined()
799+
800+
const powershellCLI = result.find(
801+
(r: unknown) =>
802+
typeof r === 'object' &&
803+
r !== null &&
804+
'title' in r &&
805+
(r as { title: string }).title ===
806+
'MyFrame - Components Responsive CLI (PowerShell)',
807+
)
808+
expect(powershellCLI).toBeDefined()
809+
})
636810
})

src/code-impl.ts

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Codegen } from './codegen/Codegen'
22
import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen'
33
import { nodeProxyTracker } from './codegen/utils/node-proxy'
4+
import { wrapComponent } from './codegen/utils/wrap-component'
45
import { exportDevup, importDevup } from './commands/devup'
56
import { exportAssets } from './commands/exportAssets'
67
import { exportComponents } from './commands/exportComponents'
78
import { getComponentName } from './utils'
9+
import { toPascal } from './utils/to-pascal'
810

911
const DEVUP_COMPONENTS = [
1012
'Center',
@@ -140,25 +142,88 @@ export function registerCodegen(ctx: typeof figma) {
140142
)
141143
}
142144

145+
// Generate responsive codes for components extracted from the page
146+
let componentsResponsiveCodes: ReadonlyArray<
147+
readonly [string, string]
148+
> = []
149+
if (componentsCodes.length > 0) {
150+
const componentNodes = codegen.getComponentNodes()
151+
const processedComponentSets = new Set<string>()
152+
const responsiveResults: Array<readonly [string, string]> = []
153+
154+
for (const componentNode of componentNodes) {
155+
// Check if the component belongs to a COMPONENT_SET
156+
const parentSet =
157+
componentNode.type === 'COMPONENT' &&
158+
componentNode.parent?.type === 'COMPONENT_SET'
159+
? (componentNode.parent as ComponentSetNode)
160+
: null
161+
162+
if (parentSet && !processedComponentSets.has(parentSet.id)) {
163+
processedComponentSets.add(parentSet.id)
164+
const componentName = getComponentName(parentSet)
165+
const responsiveCodes =
166+
await ResponsiveCodegen.generateVariantResponsiveComponents(
167+
parentSet,
168+
componentName,
169+
)
170+
responsiveResults.push(...responsiveCodes)
171+
}
172+
}
173+
componentsResponsiveCodes = responsiveResults
174+
}
175+
143176
console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`)
144177

178+
// Check if node itself is SECTION or has a parent SECTION
179+
const isNodeSection = ResponsiveCodegen.canGenerateResponsive(node)
145180
const parentSection = ResponsiveCodegen.hasParentSection(node)
181+
const sectionNode = isNodeSection
182+
? (node as SectionNode)
183+
: parentSection
184+
// When parent is Section (not node itself), use Page postfix and export default
185+
const isParentSection = !isNodeSection && parentSection !== null
146186
let responsiveResult: {
147187
title: string
148-
language: 'TYPESCRIPT'
188+
language: 'TYPESCRIPT' | 'BASH'
149189
code: string
150190
}[] = []
151191

152-
if (parentSection) {
192+
if (sectionNode) {
153193
try {
154-
const responsiveCodegen = new ResponsiveCodegen(parentSection)
194+
const responsiveCodegen = new ResponsiveCodegen(sectionNode)
155195
const responsiveCode =
156196
await responsiveCodegen.generateResponsiveCode()
197+
const baseName = toPascal(sectionNode.name)
198+
const sectionComponentName = isParentSection
199+
? `${baseName}Page`
200+
: baseName
201+
const wrappedCode = wrapComponent(
202+
sectionComponentName,
203+
responsiveCode,
204+
{ exportDefault: isParentSection },
205+
)
206+
const sectionCodes: ReadonlyArray<readonly [string, string]> = [
207+
[sectionComponentName, wrappedCode],
208+
]
209+
const importStatement = generateImportStatements(sectionCodes)
210+
const fullCode = importStatement + wrappedCode
211+
157212
responsiveResult = [
158213
{
159-
title: `${parentSection.name} - Responsive`,
214+
title: `${sectionNode.name} - Responsive`,
160215
language: 'TYPESCRIPT' as const,
161-
code: responsiveCode,
216+
code: fullCode,
217+
},
218+
{
219+
title: `${sectionNode.name} - Responsive CLI (Bash)`,
220+
language: 'BASH' as const,
221+
code: generateBashCLI(sectionCodes),
222+
},
223+
{
224+
title: `${sectionNode.name} - Responsive CLI (PowerShell)`,
225+
language: 'BASH' as const,
226+
code: generatePowerShellCLI(sectionCodes),
162227
},
163228
]
164229
} catch (e) {
@@ -202,6 +267,27 @@ export function registerCodegen(ctx: typeof figma) {
202267
},
203268
] as const)
204269
: []),
270+
...(componentsResponsiveCodes.length > 0
271+
? [
272+
{
273+
title: `${node.name} - Components Responsive`,
274+
language: 'TYPESCRIPT' as const,
275+
code: componentsResponsiveCodes
276+
.map((code) => code[1])
277+
.join('\n\n'),
278+
},
279+
{
280+
title: `${node.name} - Components Responsive CLI (Bash)`,
281+
language: 'BASH' as const,
282+
code: generateBashCLI(componentsResponsiveCodes),
283+
},
284+
{
285+
title: `${node.name} - Components Responsive CLI (PowerShell)`,
286+
language: 'BASH' as const,
287+
code: generatePowerShellCLI(componentsResponsiveCodes),
288+
},
289+
]
290+
: []),
205291
...(responsiveComponentsCodes.length > 0
206292
? [
207293
{

0 commit comments

Comments
 (0)