From 635871c6f50b9ff56f113aea36e953c40c58789e Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 21:49:40 +0900 Subject: [PATCH 01/51] Add real test --- src/code-impl.ts | 6 +- src/codegen/__tests__/codegen.test.ts | 53 +++++- .../utils/__tests__/node-proxy.test.ts | 165 +++++++++++++++++ src/codegen/utils/node-proxy.ts | 175 ++++++++++++++++++ 4 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 src/codegen/utils/__tests__/node-proxy.test.ts create mode 100644 src/codegen/utils/node-proxy.ts diff --git a/src/code-impl.ts b/src/code-impl.ts index 0b54211..0be7793 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -1,5 +1,6 @@ import { Codegen } from './codegen/Codegen' import { ResponsiveCodegen } from './codegen/responsive/ResponsiveCodegen' +import { nodeProxyTracker } from './codegen/utils/node-proxy' import { exportDevup, importDevup } from './commands/devup' import { exportAssets } from './commands/exportAssets' import { exportComponents } from './commands/exportComponents' @@ -115,7 +116,9 @@ function generatePowerShellCLI( export function registerCodegen(ctx: typeof figma) { if (ctx.editorType === 'dev' && ctx.mode === 'codegen') { - ctx.codegen.on('generate', async ({ node, language }) => { + ctx.codegen.on('generate', async ({ node: n, language }) => { + // const node = nodeProxyTracker.wrap(n) + const node = n switch (language) { case 'devup-ui': { const time = Date.now() @@ -161,6 +164,7 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } + console.log(nodeProxyTracker.toTestCaseFormat()) return [ ...(node.type === 'COMPONENT' || diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 189f733..77ab8d9 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, test } from 'bun:test' +import { afterAll, describe, expect, it, test } from 'bun:test' import { Codegen } from '../Codegen' ;(globalThis as { figma?: unknown }).figma = { @@ -4350,3 +4350,54 @@ describe('Codegen Tree Methods', () => { }) }) }) + +describe('render real world component', () => { + it.each([ + { + expected: ``, + object: { + id: '7:3', + name: 'Rectangle 2', + type: 'RECTANGLE', + reactions: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + cornerRadius: 20, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, + }, + }, + ] as const)('$title', async ({ expected, object }) => { + const codegen = new Codegen(object as unknown as SceneNode) + await codegen.run() + console.log(codegen.getCode()) + expect(codegen.getCode()).toBe(expected) + }) +}) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts new file mode 100644 index 0000000..4d877a3 --- /dev/null +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, test } from 'bun:test' +import { nodeProxyTracker } from '../node-proxy' + +// Mock SceneNode +function createMockNode(overrides: Partial = {}): SceneNode { + return { + id: 'test-node-1', + name: 'TestNode', + type: 'FRAME', + width: 100, + height: 200, + x: 10, + y: 20, + visible: true, + opacity: 1, + fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 }, opacity: 1 }], + strokes: [], + strokeWeight: 0, + cornerRadius: 8, + layoutMode: 'VERTICAL', + paddingTop: 10, + paddingRight: 10, + paddingBottom: 10, + paddingLeft: 10, + ...overrides, + } as unknown as SceneNode +} + +describe('nodeProxyTracker', () => { + beforeEach(() => { + nodeProxyTracker.clear() + }) + + test('should track property access', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + // Access some properties + const _width = wrapped.width + const _height = wrapped.height + const _name = wrapped.name + + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(1) + + const log = logs[0] + expect(log.nodeId).toBe('test-node-1') + expect(log.nodeName).toBe('TestNode') + expect(log.nodeType).toBe('FRAME') + + const accessedKeys = log.properties.map((p) => p.key) + expect(accessedKeys).toContain('width') + expect(accessedKeys).toContain('height') + expect(accessedKeys).toContain('name') + }) + + test('should serialize complex values', () => { + const node = createMockNode({ + fills: [ + { + type: 'SOLID', + color: { r: 1, g: 0, b: 0 }, + opacity: 0.5, + visible: true, + }, + ], + } as unknown as Partial) + const wrapped = nodeProxyTracker.wrap(node) + + const _fills = (wrapped as any).fills + + const log = nodeProxyTracker.getAccessLog('test-node-1') + const fillsProp = log?.properties.find((p) => p.key === 'fills') + + expect(fillsProp).toBeDefined() + expect(Array.isArray(fillsProp?.value)).toBe(true) + }) + + test('should deduplicate repeated access', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + // Access same property multiple times + const _w1 = wrapped.width + const _w2 = wrapped.width + const _w3 = wrapped.width + + const log = nodeProxyTracker.getAccessLog('test-node-1') + const widthAccesses = log?.properties.filter((p) => p.key === 'width') + + expect(widthAccesses?.length).toBe(1) + }) + + test('should output JSON format', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + const _width = wrapped.width + + const json = nodeProxyTracker.toJSON() + expect(json['test-node-1']).toBeDefined() + expect(json['test-node-1'].nodeId).toBe('test-node-1') + }) + + test('should output test case format', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + const _width = wrapped.width + const _height = wrapped.height + + const testCase = nodeProxyTracker.toTestCaseFormat() + expect(testCase['test-node-1']).toBeDefined() + expect(testCase['test-node-1'].id).toBe('test-node-1') + expect(testCase['test-node-1'].name).toBe('TestNode') + expect(testCase['test-node-1'].type).toBe('FRAME') + expect(testCase['test-node-1'].width).toBe(100) + expect(testCase['test-node-1'].height).toBe(200) + }) + + test('should clear logs', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + const _width = wrapped.width + expect(nodeProxyTracker.getAllAccessLogs().length).toBe(1) + + nodeProxyTracker.clear() + expect(nodeProxyTracker.getAllAccessLogs().length).toBe(0) + }) + + test('should exclude functions from tracking', () => { + const node = createMockNode() + ;(node as unknown as Record).someMethod = () => 'result' + const wrapped = nodeProxyTracker.wrap(node) + + const _method = (wrapped as unknown as Record string>) + .someMethod + + const log = nodeProxyTracker.getAccessLog('test-node-1') + const methodProp = log?.properties.find((p) => p.key === 'someMethod') + + expect(methodProp).toBeUndefined() + }) + + test('should track multiple nodes separately', () => { + const node1 = createMockNode({ id: 'node-1', name: 'Node1' }) + const node2 = createMockNode({ id: 'node-2', name: 'Node2' }) + + const wrapped1 = nodeProxyTracker.wrap(node1) + const wrapped2 = nodeProxyTracker.wrap(node2) + + const _w1 = wrapped1.width + const _h2 = wrapped2.height + + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(2) + + const log1 = nodeProxyTracker.getAccessLog('node-1') + const log2 = nodeProxyTracker.getAccessLog('node-2') + + expect(log1?.properties.some((p) => p.key === 'width')).toBe(true) + expect(log2?.properties.some((p) => p.key === 'height')).toBe(true) + }) +}) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts new file mode 100644 index 0000000..01be5d0 --- /dev/null +++ b/src/codegen/utils/node-proxy.ts @@ -0,0 +1,175 @@ +type AccessedProperty = { + key: string + value: unknown +} + +type AccessLog = { + nodeId: string + nodeName: string + nodeType: string + properties: AccessedProperty[] +} + +class NodeProxyTracker { + private accessLogs: Map = new Map() + + clear() { + this.accessLogs.clear() + } + + wrap(node: T): T { + const nodeId = node.id + const nodeName = node.name + const nodeType = node.type + + if (!this.accessLogs.has(nodeId)) { + this.accessLogs.set(nodeId, { + nodeId, + nodeName, + nodeType, + properties: [], + }) + } + + const log = this.accessLogs.get(nodeId) + if (!log) return node + + const accessedKeys = new Set() + + const handler: ProxyHandler = { + get: (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver) + const key = String(prop) + + // 내부 프로퍼티나 메서드는 제외 + if ( + key.startsWith('_') || + typeof value === 'function' || + key === 'then' || + key === 'toJSON' + ) { + return value + } + + // 중복 기록 방지 + if (!accessedKeys.has(key)) { + accessedKeys.add(key) + + // 값 직렬화 (circular reference 방지) + let serializedValue: unknown + try { + if (value === null || value === undefined) { + serializedValue = value + } else if (typeof value === 'object') { + const valueObj = value as Record + if (Array.isArray(value)) { + serializedValue = this.serializeArray(value) + } else if ('id' in valueObj && 'type' in valueObj) { + // 다른 노드 참조인 경우 + serializedValue = `[Node: ${valueObj.type}]` + } else { + serializedValue = this.serializeObject(valueObj) + } + } else { + serializedValue = value + } + } catch { + serializedValue = '[Unserializable]' + } + + log.properties.push({ + key, + value: serializedValue, + }) + } + + // 중첩 객체도 Proxy로 감싸기 (fills, strokes 등) + if (value && typeof value === 'object' && !Array.isArray(value)) { + const obj = value as Record + if ('id' in obj && 'type' in obj) { + // SceneNode인 경우 재귀적으로 wrap + return this.wrap(value as unknown as SceneNode) + } + } + + return value + }, + } + + return new Proxy(node, handler) + } + + private serializeArray(arr: unknown[]): unknown[] { + return arr.map((item) => { + if (item === null || item === undefined) return item + if (typeof item === 'object') { + const obj = item as Record + if ('id' in obj && 'type' in obj) { + return `[Node: ${obj.type}]` + } + return this.serializeObject(obj) + } + return item + }) + } + + private serializeObject( + obj: Record, + ): Record { + const result: Record = {} + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'function') continue + if (value === null || value === undefined) { + result[key] = value + } else if (typeof value === 'object') { + const innerObj = value as Record + if ('id' in innerObj && 'type' in innerObj) { + result[key] = `[Node: ${innerObj.type}]` + } else if (Array.isArray(value)) { + result[key] = this.serializeArray(value) + } else { + result[key] = this.serializeObject(innerObj) + } + } else { + result[key] = value + } + } + return result + } + + getAccessLog(nodeId: string): AccessLog | undefined { + return this.accessLogs.get(nodeId) + } + + getAllAccessLogs(): AccessLog[] { + return Array.from(this.accessLogs.values()) + } + + toJSON(): Record { + const result: Record = {} + for (const [id, log] of this.accessLogs) { + result[id] = log + } + return result + } + + toTestCaseFormat(): Record> { + const result: Record> = {} + for (const [id, log] of this.accessLogs) { + const props: Record = {} + for (const { key, value } of log.properties) { + props[key] = value + } + result[id] = { + id: log.nodeId, + name: log.nodeName, + type: log.nodeType, + ...props, + } + } + return result + } +} + +// 싱글톤 인스턴스 +export const nodeProxyTracker = new NodeProxyTracker() From 54435869ee9375fceebcce8937955765164e3ed5 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 22:49:11 +0900 Subject: [PATCH 02/51] Add test --- src/code-impl.ts | 8 +- src/codegen/__tests__/codegen.test.ts | 89 +++++++++-------- .../utils/__tests__/node-proxy.test.ts | 12 +-- src/codegen/utils/node-proxy.ts | 97 +++++++++++++++++-- 4 files changed, 149 insertions(+), 57 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index 0be7793..433199e 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -117,8 +117,8 @@ function generatePowerShellCLI( export function registerCodegen(ctx: typeof figma) { if (ctx.editorType === 'dev' && ctx.mode === 'codegen') { ctx.codegen.on('generate', async ({ node: n, language }) => { - // const node = nodeProxyTracker.wrap(n) - const node = n + const node = nodeProxyTracker.wrap(n) + // const node = n switch (language) { case 'devup-ui': { const time = Date.now() @@ -164,7 +164,9 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } - console.log(nodeProxyTracker.toTestCaseFormat()) + console.log( + JSON.stringify(nodeProxyTracker.toTestCaseFormat(), null, 2), + ) return [ ...(node.type === 'COMPONENT' || diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 77ab8d9..98b55f1 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect, it, test } from 'bun:test' import { Codegen } from '../Codegen' +import { assembleNodeTree, type NodeData } from '../utils/node-proxy' ;(globalThis as { figma?: unknown }).figma = { mixed: Symbol('mixed'), @@ -4355,47 +4356,57 @@ describe('render real world component', () => { it.each([ { expected: ``, - object: { - id: '7:3', - name: 'Rectangle 2', - type: 'RECTANGLE', - reactions: [], - fills: [ - { - type: 'SOLID', - visible: true, - opacity: 1, - blendMode: 'NORMAL', - color: { - r: 0.8509804010391235, - g: 0.8509804010391235, - b: 0.8509804010391235, + object: [ + { + id: '7:3', + name: 'Rectangle 2', + type: 'RECTANGLE', + reactions: [], + parent: '7:7', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, }, - boundVariables: {}, - }, - ], - isAsset: false, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, - layoutPositioning: 'AUTO', - layoutSizingVertical: 'FIXED', - layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, - width: 150, - height: 150, - cornerRadius: 20, - strokes: [], - opacity: 1, - blendMode: 'PASS_THROUGH', - effects: [], - rotation: 0, - visible: true, - }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + cornerRadius: 20, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, + }, + { + id: '7:7', + name: 'BorderRadius', + type: 'SECTION', + children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + }, + ], }, - ] as const)('$title', async ({ expected, object }) => { - const codegen = new Codegen(object as unknown as SceneNode) + ] as const)('$expected', async ({ expected, object }) => { + const root = assembleNodeTree(object as unknown as NodeData[]) + const codegen = new Codegen(root as unknown as SceneNode) await codegen.run() console.log(codegen.getCode()) expect(codegen.getCode()).toBe(expected) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index 4d877a3..f46dfba 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -110,12 +110,12 @@ describe('nodeProxyTracker', () => { const _height = wrapped.height const testCase = nodeProxyTracker.toTestCaseFormat() - expect(testCase['test-node-1']).toBeDefined() - expect(testCase['test-node-1'].id).toBe('test-node-1') - expect(testCase['test-node-1'].name).toBe('TestNode') - expect(testCase['test-node-1'].type).toBe('FRAME') - expect(testCase['test-node-1'].width).toBe(100) - expect(testCase['test-node-1'].height).toBe(200) + expect(testCase.length).toBe(1) + expect(testCase[0].id).toBe('test-node-1') + expect(testCase[0].name).toBe('TestNode') + expect(testCase[0].type).toBe('FRAME') + expect(testCase[0].width).toBe(100) + expect(testCase[0].height).toBe(200) }) test('should clear logs', () => { diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 01be5d0..1f6e640 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -65,8 +65,8 @@ class NodeProxyTracker { if (Array.isArray(value)) { serializedValue = this.serializeArray(value) } else if ('id' in valueObj && 'type' in valueObj) { - // 다른 노드 참조인 경우 - serializedValue = `[Node: ${valueObj.type}]` + // 다른 노드 참조인 경우 - id로 저장 + serializedValue = `[NodeId: ${valueObj.id}]` } else { serializedValue = this.serializeObject(valueObj) } @@ -105,7 +105,7 @@ class NodeProxyTracker { if (typeof item === 'object') { const obj = item as Record if ('id' in obj && 'type' in obj) { - return `[Node: ${obj.type}]` + return `[NodeId: ${obj.id}]` } return this.serializeObject(obj) } @@ -124,7 +124,7 @@ class NodeProxyTracker { } else if (typeof value === 'object') { const innerObj = value as Record if ('id' in innerObj && 'type' in innerObj) { - result[key] = `[Node: ${innerObj.type}]` + result[key] = `[NodeId: ${innerObj.id}]` } else if (Array.isArray(value)) { result[key] = this.serializeArray(value) } else { @@ -153,23 +153,102 @@ class NodeProxyTracker { return result } - toTestCaseFormat(): Record> { - const result: Record> = {} - for (const [id, log] of this.accessLogs) { + toTestCaseFormat(): NodeData[] { + const result: NodeData[] = [] + for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { - props[key] = value + props[key] = this.resolveNodeRefs(value) } - result[id] = { + result.push({ id: log.nodeId, name: log.nodeName, type: log.nodeType, ...props, + }) + } + return result + } + + private resolveNodeRefs(value: unknown): unknown { + if (typeof value === 'string' && value.startsWith('[NodeId: ')) { + const match = value.match(/\[NodeId: ([^\]]+)\]/) + if (match) { + return match[1] } } + if (Array.isArray(value)) { + return value.map((item) => this.resolveNodeRefs(item)) + } + return value + } + + /** + * Returns a flat list of node objects for test cases. + * Use assembleNodeTree() to link parent/children relationships. + */ + toNodeList(): Record[] { + const result: Record[] = [] + for (const log of this.accessLogs.values()) { + const props: Record = {} + for (const { key, value } of log.properties) { + props[key] = this.resolveNodeRefs(value) + } + result.push({ + id: log.nodeId, + name: log.nodeName, + type: log.nodeType, + ...props, + }) + } return result } } +export type NodeData = Record & { + id: string + name: string + type: string + parent?: string | NodeData + children?: (string | NodeData)[] +} + +/** + * Assembles a node tree from a flat list of nodes. + * Links parent/children by id references. + */ +export function assembleNodeTree(nodes: NodeData[]): NodeData { + const nodeMap = new Map() + + // 1. 모든 노드를 복사해서 맵에 저장 + for (const node of nodes) { + nodeMap.set(node.id, { ...node }) + } + + // 2. parent/children 관계 연결 + for (const node of nodeMap.values()) { + // parent 연결 + if (typeof node.parent === 'string') { + const parentNode = nodeMap.get(node.parent) + if (parentNode) { + node.parent = parentNode + } + } + + // children 연결 + if (Array.isArray(node.children)) { + node.children = node.children.map((childId) => { + if (typeof childId === 'string') { + return nodeMap.get(childId) || childId + } + return childId + }) + } + } + + // 3. 첫 번째 노드(루트) 반환 + return nodeMap.get(nodes[0].id) || nodes[0] +} + // 싱글톤 인스턴스 export const nodeProxyTracker = new NodeProxyTracker() From f0725e87af587adcaf55f9f739b101b37322a8c7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 23:28:11 +0900 Subject: [PATCH 03/51] Add radius case --- src/code-impl.ts | 4 +- src/codegen/__tests__/codegen.test.ts | 108 ++++++++++++++++++++++++++ src/codegen/utils/node-proxy.ts | 22 +++--- 3 files changed, 120 insertions(+), 14 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index 433199e..3b866e5 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -164,9 +164,7 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } - console.log( - JSON.stringify(nodeProxyTracker.toTestCaseFormat(), null, 2), - ) + console.log(nodeProxyTracker.toTestCaseFormat()) return [ ...(node.type === 'COMPONENT' || diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 98b55f1..a0b9c38 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4404,6 +4404,114 @@ describe('render real world component', () => { }, ], }, + { + expected: + '', + object: [ + { + id: '6:2', + name: 'Rectangle 1', + type: 'RECTANGLE', + reactions: [], + parent: '7:7', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + topLeftRadius: 20, + topRightRadius: 30, + bottomRightRadius: 50, + bottomLeftRadius: 40, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, + }, + { + id: '7:7', + name: 'BorderRadius', + type: 'SECTION', + children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + }, + ], + }, + { + expected: + '', + object: [ + { + id: '7:5', + name: 'Rectangle 3', + type: 'RECTANGLE', + reactions: [], + parent: '7:7', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + topLeftRadius: 200, + topRightRadius: 30, + bottomRightRadius: 200, + bottomLeftRadius: 30, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, + }, + { + id: '7:7', + name: 'BorderRadius', + type: 'SECTION', + children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + }, + ], + }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 1f6e640..274d773 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -227,22 +227,22 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { // 2. parent/children 관계 연결 for (const node of nodeMap.values()) { - // parent 연결 + // parent 연결 (없으면 undefined로 설정) if (typeof node.parent === 'string') { const parentNode = nodeMap.get(node.parent) - if (parentNode) { - node.parent = parentNode - } + node.parent = parentNode } - // children 연결 + // children 연결 (없는 노드는 필터링) if (Array.isArray(node.children)) { - node.children = node.children.map((childId) => { - if (typeof childId === 'string') { - return nodeMap.get(childId) || childId - } - return childId - }) + node.children = node.children + .map((childId) => { + if (typeof childId === 'string') { + return nodeMap.get(childId) + } + return childId + }) + .filter((child): child is NodeData => child !== undefined) } } From 30c3019eedd1a6db677087f5d8a9630608de0bd5 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:04:54 +0900 Subject: [PATCH 04/51] Refactor --- src/codegen/__tests__/codegen.test.ts | 136 +++++++++++++++++++++++++ src/codegen/utils/node-proxy.ts | 138 ++++++++++++++++++++++++-- 2 files changed, 264 insertions(+), 10 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index a0b9c38..2469356 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4512,8 +4512,144 @@ describe('render real world component', () => { }, ], }, + { + expected: ` + +`, + object: [ + { + id: '496:2019', + name: 'Frame 1597884473', + type: 'FRAME', + reactions: [], + children: ['8:2'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { r: 1, g: 1, b: 1 }, + boundVariables: {}, + }, + ], + width: 175, + height: 223, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'FIXED', + clipsContent: true, + visible: true, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 57, + paddingRight: 118, + paddingTop: 51, + paddingBottom: 22, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + parent: '10:14', + }, + { + id: '8:2', + name: 'Linear', + type: 'FRAME', + reactions: [], + parent: '496:2019', + children: [], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.776627242565155, + g: 0.789053201675415, + b: 0.807692289352417, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + isAsset: false, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + cornerRadius: 1000, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + }, + ], + }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) + console.log(root) const codegen = new Codegen(root as unknown as SceneNode) await codegen.run() console.log(codegen.getCode()) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 274d773..525e7b3 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -83,12 +83,31 @@ class NodeProxyTracker { }) } - // 중첩 객체도 Proxy로 감싸기 (fills, strokes 등) - if (value && typeof value === 'object' && !Array.isArray(value)) { - const obj = value as Record - if ('id' in obj && 'type' in obj) { - // SceneNode인 경우 재귀적으로 wrap - return this.wrap(value as unknown as SceneNode) + // 중첩 객체도 Proxy로 감싸기 + if (value && typeof value === 'object') { + // children 배열인 경우 각 child를 재귀적으로 추적 + if (key === 'children' && Array.isArray(value)) { + return value.map((child) => { + if ( + child && + typeof child === 'object' && + 'id' in child && + 'type' in child + ) { + const wrappedChild = this.wrap(child as unknown as SceneNode) + // child의 모든 프로퍼티를 강제로 접근해서 추적 + this.trackNodeRecursively(child as unknown as SceneNode) + return wrappedChild + } + return child + }) + } + // SceneNode인 경우 재귀적으로 wrap + if (!Array.isArray(value)) { + const obj = value as Record + if ('id' in obj && 'type' in obj) { + return this.wrap(value as unknown as SceneNode) + } } } @@ -99,6 +118,64 @@ class NodeProxyTracker { return new Proxy(node, handler) } + private trackNodeRecursively(node: SceneNode): void { + const wrappedNode = this.wrap(node) + + // 주요 프로퍼티들에 접근해서 추적 + const propsToTrack = [ + 'id', + 'name', + 'type', + 'visible', + 'parent', + 'children', + 'fills', + 'strokes', + 'effects', + 'opacity', + 'blendMode', + 'width', + 'height', + 'rotation', + 'cornerRadius', + 'topLeftRadius', + 'topRightRadius', + 'bottomLeftRadius', + 'bottomRightRadius', + 'layoutMode', + 'layoutAlign', + 'layoutGrow', + 'layoutSizingHorizontal', + 'layoutSizingVertical', + 'layoutPositioning', + 'primaryAxisAlignItems', + 'counterAxisAlignItems', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'itemSpacing', + 'counterAxisSpacing', + 'clipsContent', + 'isAsset', + 'reactions', + 'minWidth', + 'maxWidth', + 'minHeight', + 'maxHeight', + 'targetAspectRatio', + 'inferredAutoLayout', + ] + + for (const prop of propsToTrack) { + try { + void (wrappedNode as unknown as Record)[prop] + } catch { + // ignore + } + } + } + private serializeArray(arr: unknown[]): unknown[] { return arr.map((item) => { if (item === null || item === undefined) return item @@ -107,12 +184,29 @@ class NodeProxyTracker { if ('id' in obj && 'type' in obj) { return `[NodeId: ${obj.id}]` } + // 숫자 키만 있는 객체는 배열로 변환 (gradientTransform 등) + if (this.isArrayLikeObject(obj)) { + return this.arrayLikeToArray(obj) + } return this.serializeObject(obj) } return item }) } + private isArrayLikeObject(obj: Record): boolean { + const keys = Object.keys(obj) + if (keys.length === 0) return false + return keys.every((key) => /^\d+$/.test(key)) + } + + private arrayLikeToArray(obj: Record): unknown[] { + const keys = Object.keys(obj) + .map(Number) + .sort((a, b) => a - b) + return keys.map((key) => obj[key]) + } + private serializeObject( obj: Record, ): Record { @@ -153,20 +247,38 @@ class NodeProxyTracker { return result } - toTestCaseFormat(): NodeData[] { + /** + * Returns nodes as array with the root node first. + * @param rootId - The ID of the root node (clicked node) to put first + */ + toTestCaseFormat(rootId?: string): NodeData[] { const result: NodeData[] = [] + let rootNode: NodeData | null = null + for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { props[key] = this.resolveNodeRefs(value) } - result.push({ + const node: NodeData = { id: log.nodeId, name: log.nodeName, type: log.nodeType, ...props, - }) + } + + if (rootId && log.nodeId === rootId) { + rootNode = node + } else { + result.push(node) + } + } + + // 루트 노드를 맨 앞에 배치 + if (rootNode) { + result.unshift(rootNode) } + return result } @@ -230,7 +342,12 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { // parent 연결 (없으면 undefined로 설정) if (typeof node.parent === 'string') { const parentNode = nodeMap.get(node.parent) - node.parent = parentNode + // SECTION은 테스트용 컨테이너이므로 parent로 연결하지 않음 + if (parentNode && parentNode.type === 'SECTION') { + node.parent = undefined + } else { + node.parent = parentNode + } } // children 연결 (없는 노드는 필터링) @@ -244,6 +361,7 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { }) .filter((child): child is NodeData => child !== undefined) } + // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) } // 3. 첫 번째 노드(루트) 반환 From 4813bb3425f0e29b456dc659bab7fba2212bba93 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:21:23 +0900 Subject: [PATCH 05/51] Fix case --- src/code-impl.ts | 2 +- src/codegen/__tests__/codegen.test.ts | 184 +++++++++++++++++--------- src/codegen/utils/node-proxy.ts | 66 ++++++--- 3 files changed, 170 insertions(+), 82 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index 3b866e5..ae398a7 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -164,7 +164,7 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } - console.log(nodeProxyTracker.toTestCaseFormat()) + console.log(nodeProxyTracker.toTestCaseFormat(node.id)) return [ ...(node.type === 'COMPONENT' || diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 2469356..35becd3 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4395,6 +4395,12 @@ describe('render real world component', () => { effects: [], rotation: 0, visible: true, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutAlign: 'INHERIT', + layoutGrow: 0, }, { id: '7:7', @@ -4412,7 +4418,7 @@ describe('render real world component', () => { id: '6:2', name: 'Rectangle 1', type: 'RECTANGLE', - reactions: [], + visible: true, parent: '7:7', fills: [ { @@ -4428,33 +4434,35 @@ describe('render real world component', () => { boundVariables: {}, }, ], - isAsset: false, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, - layoutPositioning: 'AUTO', - layoutSizingVertical: 'FIXED', - layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', width: 150, height: 150, + rotation: 0, topLeftRadius: 20, topRightRadius: 30, - bottomRightRadius: 50, bottomLeftRadius: 40, - strokes: [], - opacity: 1, - blendMode: 'PASS_THROUGH', - effects: [], - rotation: 0, - visible: true, + bottomRightRadius: 50, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, }, { id: '7:7', name: 'BorderRadius', type: 'SECTION', - children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + children: ['6:2'], }, ], }, @@ -4466,7 +4474,7 @@ describe('render real world component', () => { id: '7:5', name: 'Rectangle 3', type: 'RECTANGLE', - reactions: [], + visible: true, parent: '7:7', fills: [ { @@ -4482,33 +4490,35 @@ describe('render real world component', () => { boundVariables: {}, }, ], - isAsset: false, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, - layoutPositioning: 'AUTO', - layoutSizingVertical: 'FIXED', - layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', width: 150, height: 150, + rotation: 0, topLeftRadius: 200, topRightRadius: 30, - bottomRightRadius: 200, bottomLeftRadius: 30, - strokes: [], - opacity: 1, - blendMode: 'PASS_THROUGH', - effects: [], - rotation: 0, - visible: true, + bottomRightRadius: 200, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, }, { id: '7:7', name: 'BorderRadius', type: 'SECTION', - children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + children: ['7:5'], }, ], }, @@ -4530,23 +4540,26 @@ describe('render real world component', () => { name: 'Frame 1597884473', type: 'FRAME', reactions: [], + parent: '10:14', children: ['8:2'], + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, fills: [ { type: 'SOLID', visible: true, opacity: 1, blendMode: 'NORMAL', - color: { r: 1, g: 1, b: 1 }, + color: { + r: 1, + g: 1, + b: 1, + }, boundVariables: {}, }, ], - width: 175, - height: 223, - layoutSizingHorizontal: 'HUG', - layoutSizingVertical: 'FIXED', - clipsContent: true, - visible: true, inferredAutoLayout: { layoutMode: 'HORIZONTAL', paddingLeft: 57, @@ -4562,13 +4575,42 @@ describe('render real world component', () => { itemSpacing: 0, layoutPositioning: 'AUTO', }, - parent: '10:14', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + height: 223, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 325, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + counterAxisSpacing: 0, }, { id: '8:2', name: 'Linear', type: 'FRAME', - reactions: [], + visible: true, parent: '496:2019', children: [], fills: [ @@ -4605,7 +4647,40 @@ describe('render real world component', () => { ], }, ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 150, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -4621,29 +4696,12 @@ describe('render real world component', () => { itemSpacing: 0, layoutPositioning: 'AUTO', }, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, - layoutPositioning: 'AUTO', - layoutSizingVertical: 'FIXED', - layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, - width: 150, - height: 150, - cornerRadius: 1000, - strokes: [], - opacity: 1, - blendMode: 'PASS_THROUGH', - effects: [], - rotation: 0, - clipsContent: true, - visible: true, }, { id: '10:14', name: 'Gradient', type: 'SECTION', + children: ['496:2019'], }, ], }, diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 525e7b3..72d3e18 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -249,34 +249,69 @@ class NodeProxyTracker { /** * Returns nodes as array with the root node first. - * @param rootId - The ID of the root node (clicked node) to put first + * @param rootId - The ID of the root node (clicked node) to put first. + * Only includes this node and its descendants. */ toTestCaseFormat(rootId?: string): NodeData[] { - const result: NodeData[] = [] - let rootNode: NodeData | null = null - + // 모든 노드를 먼저 변환 + const allNodes: NodeData[] = [] for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { props[key] = this.resolveNodeRefs(value) } - const node: NodeData = { + allNodes.push({ id: log.nodeId, name: log.nodeName, type: log.nodeType, ...props, + }) + } + + if (!rootId) { + return allNodes + } + + // rootId가 주어진 경우: 루트 노드와 그 하위 노드들만 필터링 + const rootNode = allNodes.find((n) => n.id === rootId) + if (!rootNode) { + return allNodes + } + + // 하위 노드 ID들을 수집 (children을 재귀적으로 탐색) + const descendantIds = new Set() + const collectDescendants = (nodeId: string) => { + const node = allNodes.find((n) => n.id === nodeId) + if (!node) return + if (Array.isArray(node.children)) { + for (const childId of node.children) { + if (typeof childId === 'string') { + descendantIds.add(childId) + collectDescendants(childId) + } + } } + } + collectDescendants(rootId) - if (rootId && log.nodeId === rootId) { - rootNode = node - } else { + // 루트 노드와 하위 노드들만 필터링 + const result: NodeData[] = [rootNode] + for (const node of allNodes) { + if (node.id !== rootId && descendantIds.has(node.id)) { result.push(node) } } - // 루트 노드를 맨 앞에 배치 - if (rootNode) { - result.unshift(rootNode) + // 부모 노드도 포함 (SECTION 타입만) + const parentId = + typeof rootNode.parent === 'string' ? rootNode.parent : undefined + if (parentId) { + const parentNode = allNodes.find((n) => n.id === parentId) + if (parentNode && parentNode.type === 'SECTION') { + // 부모의 children에서 루트 노드만 남기기 + parentNode.children = [rootId] + result.push(parentNode) + } } return result @@ -339,15 +374,10 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { // 2. parent/children 관계 연결 for (const node of nodeMap.values()) { - // parent 연결 (없으면 undefined로 설정) + // parent 연결 if (typeof node.parent === 'string') { const parentNode = nodeMap.get(node.parent) - // SECTION은 테스트용 컨테이너이므로 parent로 연결하지 않음 - if (parentNode && parentNode.type === 'SECTION') { - node.parent = undefined - } else { - node.parent = parentNode - } + node.parent = parentNode } // children 연결 (없는 노드는 필터링) From 7bb743e16bc8d4b1a4a6ea7dd2fedc239cce5a21 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:33:15 +0900 Subject: [PATCH 06/51] Add case --- src/codegen/__tests__/codegen.test.ts | 432 ++++++++++++++++++++++++-- 1 file changed, 398 insertions(+), 34 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 35becd3..c3b5a3e 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4406,7 +4406,7 @@ describe('render real world component', () => { id: '7:7', name: 'BorderRadius', type: 'SECTION', - children: ['6:2', '7:5', '7:3', '7:8', '7:12', '7:16'], + children: ['7:3'], }, ], }, @@ -4418,7 +4418,7 @@ describe('render real world component', () => { id: '6:2', name: 'Rectangle 1', type: 'RECTANGLE', - visible: true, + reactions: [], parent: '7:7', fills: [ { @@ -4434,29 +4434,29 @@ describe('render real world component', () => { boundVariables: {}, }, ], - strokes: [], - effects: [], - opacity: 1, - blendMode: 'PASS_THROUGH', + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, width: 150, height: 150, - rotation: 0, topLeftRadius: 20, topRightRadius: 30, - bottomLeftRadius: 40, bottomRightRadius: 50, + bottomLeftRadius: 40, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, layoutAlign: 'INHERIT', layoutGrow: 0, - layoutSizingHorizontal: 'FIXED', - layoutSizingVertical: 'FIXED', - layoutPositioning: 'AUTO', - isAsset: false, - reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, }, { id: '7:7', @@ -4540,6 +4540,21 @@ describe('render real world component', () => { name: 'Frame 1597884473', type: 'FRAME', reactions: [], + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 57, + paddingRight: 118, + paddingTop: 51, + paddingBottom: 22, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, parent: '10:14', children: ['8:2'], paddingLeft: 0, @@ -4560,12 +4575,363 @@ describe('render real world component', () => { boundVariables: {}, }, ], + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + height: 223, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 325, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '8:2', + name: 'Linear', + type: 'FRAME', + reactions: [], + parent: '496:2019', + children: [], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.776627242565155, + g: 0.789053201675415, + b: 0.807692289352417, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + isAsset: false, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + cornerRadius: 1000, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2019'], + }, + ], + }, + { + expected: ` + +`, + object: [ + { + id: '496:2050', + name: 'Frame 1597884474', + type: 'FRAME', + reactions: [], inferredAutoLayout: { layoutMode: 'HORIZONTAL', - paddingLeft: 57, - paddingRight: 118, - paddingTop: 51, - paddingBottom: 22, + paddingLeft: 147, + paddingRight: 110, + paddingTop: 49, + paddingBottom: 16, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + parent: '10:14', + visible: true, + children: ['14:23'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 407, + height: 215, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + }, + { + id: '14:23', + name: 'Linear', + type: 'FRAME', + reactions: [], + parent: '496:2050', + children: [], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.776627242565155, + g: 0.789053201675415, + b: 0.807692289352417, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [-0.9584663510322571, -0.8847382068634033, 1.2973703145980835], + [0.8847382068634033, -0.9584663510322571, 0.5272794365882874], + ], + }, + ], + isAsset: false, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: null, + width: 150, + height: 150, + cornerRadius: 1000, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2050'], + }, + ], + }, + { + expected: ` + +`, + object: [ + { + id: '496:2051', + name: 'Frame 1597884475', + type: 'FRAME', + reactions: [], + parent: '10:14', + children: ['8:6'], + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 102, + paddingRight: 110, + paddingTop: 70, + paddingBottom: 34, counterAxisSizingMode: 'AUTO', primaryAxisSizingMode: 'AUTO', primaryAxisAlignItems: 'MIN', @@ -4585,7 +4951,7 @@ describe('render real world component', () => { layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', targetAspectRatio: null, - height: 223, + height: 254, cornerRadius: 0, topLeftRadius: 0, topRightRadius: 0, @@ -4599,7 +4965,7 @@ describe('render real world component', () => { rotation: 0, clipsContent: true, visible: true, - width: 325, + width: 362, layoutMode: 'NONE', layoutAlign: 'INHERIT', layoutGrow: 0, @@ -4607,15 +4973,15 @@ describe('render real world component', () => { counterAxisSpacing: 0, }, { - id: '8:2', - name: 'Linear', + id: '8:6', + name: 'Radial', type: 'FRAME', visible: true, - parent: '496:2019', + parent: '496:2051', children: [], fills: [ { - type: 'GRADIENT_LINEAR', + type: 'GRADIENT_RADIAL', visible: true, opacity: 1, blendMode: 'NORMAL', @@ -4632,9 +4998,9 @@ describe('render real world component', () => { }, { color: { - r: 0.776627242565155, - g: 0.789053201675415, - b: 0.807692289352417, + r: 0.8653846383094788, + g: 0.9192305207252502, + b: 1, a: 1, }, position: 1, @@ -4701,16 +5067,14 @@ describe('render real world component', () => { id: '10:14', name: 'Gradient', type: 'SECTION', - children: ['496:2019'], + children: ['496:2051'], }, ], }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) - console.log(root) const codegen = new Codegen(root as unknown as SceneNode) await codegen.run() - console.log(codegen.getCode()) expect(codegen.getCode()).toBe(expected) }) }) From beca35115e6db9a92ac917fdf483d694d58f50c1 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:37:44 +0900 Subject: [PATCH 07/51] Add all of gradient test --- src/codegen/__tests__/codegen.test.ts | 732 ++++++++++++++++++++++++++ 1 file changed, 732 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index c3b5a3e..475bd36 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -5071,6 +5071,738 @@ describe('render real world component', () => { }, ], }, + { + expected: ` + +`, + object: [ + { + id: '496:2054', + name: 'Frame 1597884476', + type: 'FRAME', + visible: true, + parent: '10:14', + children: ['16:28'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 402, + height: 259, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 152, + paddingRight: 100, + paddingTop: 83, + paddingBottom: 26, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '16:28', + name: 'Radial', + type: 'FRAME', + visible: true, + parent: '496:2054', + children: [], + fills: [ + { + type: 'GRADIENT_RADIAL', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.8653846383094788, + g: 0.9192305207252502, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [-1.3973798751831055, 0.4803493320941925, 0.7598253488540649], + [-0.4803493320941925, -1.3973798751831055, 1.1986899375915527], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 150, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2054'], + }, + ], + }, + { + expected: ` + +`, + object: [ + { + id: '496:2055', + name: 'Frame 1597884477', + type: 'FRAME', + visible: true, + parent: '10:14', + children: ['10:2'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 290, + height: 225, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 71, + paddingRight: 69, + paddingTop: 37, + paddingBottom: 38, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:2', + name: 'Conic', + type: 'FRAME', + visible: true, + parent: '496:2055', + children: [], + fills: [ + { + type: 'GRADIENT_ANGULAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.8653846383094788, + g: 0.9192305207252502, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 150, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2055'], + }, + ], + }, + { + expected: ` + +`, + object: [ + { + id: '496:2056', + name: 'Frame 1597884478', + type: 'FRAME', + visible: true, + parent: '10:14', + children: ['10:6'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 311, + height: 199, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 56, + paddingRight: 105, + paddingTop: 27, + paddingBottom: 22, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:6', + name: 'Conic 2', + type: 'FRAME', + visible: true, + parent: '496:2056', + children: [], + fills: [ + { + type: 'GRADIENT_ANGULAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.8653846383094788, + g: 0.9192305207252502, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [1.2016550432044563e-15, -0.5, 1], + [0.5, 1.2681746581347047e-15, 0.25], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 150, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2056'], + }, + ], + }, + { + expected: ` + +`, + object: [ + { + id: '496:2057', + name: 'Frame 1597884479', + type: 'FRAME', + visible: true, + parent: '10:14', + children: ['10:10'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 332, + height: 239, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 108, + paddingRight: 74, + paddingTop: 58, + paddingBottom: 31, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:10', + name: 'Diamond', + type: 'FRAME', + visible: true, + parent: '496:2057', + children: [], + fills: [ + { + type: 'GRADIENT_DIAMOND', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5416664481163025, + g: 0.8548610210418701, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.8653846383094788, + g: 0.9192305207252502, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [7.288291606517838e-15, -1, 1], + [1, 1.6481708965692841e-15, -5.329070518200751e-15], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 150, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '10:14', + name: 'Gradient', + type: 'SECTION', + children: ['496:2057'], + }, + ], + }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) From 81e50df193a93834868e6a94d3e170ebad6c4314 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:46:43 +0900 Subject: [PATCH 08/51] Add all of gradient test --- src/codegen/__tests__/codegen.test.ts | 151 +++++++++++--------------- src/codegen/utils/node-proxy.ts | 2 + 2 files changed, 68 insertions(+), 85 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 475bd36..7f6e61f 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4378,14 +4378,9 @@ describe('render real world component', () => { }, ], isAsset: false, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, width: 150, height: 150, cornerRadius: 20, @@ -4435,14 +4430,9 @@ describe('render real world component', () => { }, ], isAsset: false, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, width: 150, height: 150, topLeftRadius: 20, @@ -4508,11 +4498,6 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, }, { id: '7:7', @@ -4522,6 +4507,7 @@ describe('render real world component', () => { }, ], }, + // gradient { expected: ` { ], primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, height: 223, cornerRadius: 0, topLeftRadius: 0, @@ -4663,14 +4644,9 @@ describe('render real world component', () => { itemSpacing: 0, layoutPositioning: 'AUTO', }, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, width: 150, height: 150, cornerRadius: 1000, @@ -4783,11 +4759,6 @@ describe('render real world component', () => { counterAxisSpacing: 0, clipsContent: true, isAsset: false, - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, }, { id: '14:23', @@ -4846,14 +4817,9 @@ describe('render real world component', () => { itemSpacing: 0, layoutPositioning: 'AUTO', }, - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, width: 150, height: 150, cornerRadius: 1000, @@ -4943,14 +4909,9 @@ describe('render real world component', () => { }, primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', - targetAspectRatio: null, height: 254, cornerRadius: 0, topLeftRadius: 0, @@ -5042,11 +5003,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -5134,11 +5090,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'HORIZONTAL', paddingLeft: 152, @@ -5225,11 +5176,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -5317,11 +5263,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'HORIZONTAL', paddingLeft: 71, @@ -5408,11 +5349,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -5500,11 +5436,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'HORIZONTAL', paddingLeft: 56, @@ -5591,11 +5522,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -5683,11 +5609,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'HORIZONTAL', paddingLeft: 108, @@ -5774,11 +5695,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: false, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, - targetAspectRatio: null, inferredAutoLayout: { layoutMode: 'NONE', paddingLeft: 0, @@ -5803,6 +5719,71 @@ describe('render real world component', () => { }, ], }, + // // text + // { + // expected: ` + // Hello World + // `, + // object: [ + // { + // id: '35:7', + // name: 'Hello World', + // type: 'TEXT', + // maxWidth: null, + // maxHeight: null, + // minWidth: null, + // minHeight: null, + // parent: '35:2', + // layoutPositioning: 'AUTO', + // layoutSizingVertical: 'FIXED', + // layoutSizingHorizontal: 'FIXED', + // textAutoResize: 'WIDTH_AND_HEIGHT', + // strokes: [], + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 1, + // g: 1, + // b: 1, + // }, + // boundVariables: {}, + // }, + // ], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // characters: 'Hello World', + // isAsset: false, + // maxLines: null, + // textTruncation: 'DISABLED', + // effects: [], + // rotation: 0, + // reactions: [], + // visible: true, + // width: 65, + // height: 15, + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // targetAspectRatio: null, + // }, + // { + // id: '35:2', + // name: 'Text', + // type: 'SECTION', + // children: ['35:7'], + // }, + // ], + // }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 72d3e18..80345bb 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -258,6 +258,8 @@ class NodeProxyTracker { for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { + // null 값은 제외 + if (value === null) continue props[key] = this.resolveNodeRefs(value) } allNodes.push({ From b178991517217995f3d6db885882fe81889a2f35 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 01:09:56 +0900 Subject: [PATCH 09/51] Add Text test --- src/codegen/__tests__/codegen.test.ts | 287 ++++++++++++++++++++------ src/codegen/utils/node-proxy.ts | 53 ++++- 2 files changed, 274 insertions(+), 66 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 7f6e61f..764bc4f 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -5719,71 +5719,228 @@ describe('render real world component', () => { }, ], }, - // // text - // { - // expected: ` - // Hello World - // `, - // object: [ - // { - // id: '35:7', - // name: 'Hello World', - // type: 'TEXT', - // maxWidth: null, - // maxHeight: null, - // minWidth: null, - // minHeight: null, - // parent: '35:2', - // layoutPositioning: 'AUTO', - // layoutSizingVertical: 'FIXED', - // layoutSizingHorizontal: 'FIXED', - // textAutoResize: 'WIDTH_AND_HEIGHT', - // strokes: [], - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 1, - // g: 1, - // b: 1, - // }, - // boundVariables: {}, - // }, - // ], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // characters: 'Hello World', - // isAsset: false, - // maxLines: null, - // textTruncation: 'DISABLED', - // effects: [], - // rotation: 0, - // reactions: [], - // visible: true, - // width: 65, - // height: 15, - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // targetAspectRatio: null, - // }, - // { - // id: '35:2', - // name: 'Text', - // type: 'SECTION', - // children: ['35:7'], - // }, - // ], - // }, + // text + { + expected: ` + Hello World +`, + object: [ + { + id: '35:7', + name: 'Hello World', + type: 'TEXT', + parent: '35:2', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + textAutoResize: 'WIDTH_AND_HEIGHT', + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + characters: 'Hello World', + isAsset: false, + textTruncation: 'DISABLED', + effects: [], + rotation: 0, + reactions: [], + visible: true, + width: 65, + height: 15, + layoutAlign: 'INHERIT', + layoutGrow: 0, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + }, + { + id: '35:2', + name: 'Text', + type: 'SECTION', + children: ['35:7'], + }, + ], + }, + { + expected: ` + Hello World +`, + object: [ + { + id: '35:12', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '35:2', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '35:2', + name: 'Text', + type: 'SECTION', + children: ['35:12'], + }, + ], + }, + { + expected: ` + Hello World +`, + object: [ + { + id: '35:18', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '35:2', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 200, + height: 200, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'NONE', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '35:2', + name: 'Text', + type: 'SECTION', + children: ['35:18'], + }, + ], + }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 80345bb..c1b78af 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -165,6 +165,18 @@ class NodeProxyTracker { 'maxHeight', 'targetAspectRatio', 'inferredAutoLayout', + // TEXT 노드 속성 + 'characters', + 'fontName', + 'fontSize', + 'fontWeight', + 'lineHeight', + 'letterSpacing', + 'textAutoResize', + 'textAlignHorizontal', + 'textAlignVertical', + 'textTruncation', + 'maxLines', ] for (const prop of propsToTrack) { @@ -374,7 +386,7 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { nodeMap.set(node.id, { ...node }) } - // 2. parent/children 관계 연결 + // 2. parent/children 관계 연결 및 TEXT 노드 mock 메서드 추가 for (const node of nodeMap.values()) { // parent 연결 if (typeof node.parent === 'string') { @@ -394,6 +406,45 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { .filter((child): child is NodeData => child !== undefined) } // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) + + // TEXT 노드에 getStyledTextSegments mock 메서드 추가 + if (node.type === 'TEXT') { + const textNode = node as NodeData & { styledTextSegments?: unknown[] } + ;(node as unknown as Record).getStyledTextSegments = + () => { + // 테스트 데이터에 styledTextSegments가 있으면 사용 + if (textNode.styledTextSegments) { + return textNode.styledTextSegments + } + // 없으면 기본 세그먼트 생성 + return [ + { + characters: (node.characters as string) || '', + start: 0, + end: ((node.characters as string) || '').length, + fontName: (node.fontName as { family: string }) || { + family: 'Inter', + style: 'Regular', + }, + fontWeight: (node.fontWeight as number) || 400, + fontSize: (node.fontSize as number) || 12, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: (node.lineHeight as unknown) || { unit: 'AUTO' }, + letterSpacing: (node.letterSpacing as unknown) || { + unit: 'PERCENT', + value: 0, + }, + fills: (node.fills as unknown[]) || [], + textStyleId: '', + fillStyleId: '', + listOptions: { type: 'NONE' }, + indentation: 0, + hyperlink: null, + }, + ] + } + } } // 3. 첫 번째 노드(루트) 반환 From 81996ac08677e610030c63df691b6fec3a8b91d3 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 01:11:13 +0900 Subject: [PATCH 10/51] Add Text test --- src/codegen/__tests__/codegen.test.ts | 151 ++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 764bc4f..404908e 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -5941,6 +5941,157 @@ describe('render real world component', () => { }, ], }, + { + expected: ` + Hello World +`, + object: [ + { + id: '41:7', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '35:2', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'RIGHT', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + }, + { + id: '35:2', + name: 'Text', + type: 'SECTION', + children: ['41:7'], + }, + ], + }, + { + expected: ` + Hello World +`, + object: [ + { + id: '41:12', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '35:2', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 200, + height: 200, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'NONE', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + }, + { + id: '35:2', + name: 'Text', + type: 'SECTION', + children: ['41:12'], + }, + ], + }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) From 3c5dd15353d32fb4ea133a9ac72484b6fbd7ccd4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 01:18:37 +0900 Subject: [PATCH 11/51] Add auto layout test --- src/codegen/__tests__/codegen.test.ts | 821 ++++++++++++++++++++++++++ 1 file changed, 821 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 404908e..2ec30a5 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -6092,6 +6092,827 @@ describe('render real world component', () => { }, ], }, + // auto layout + { + expected: ` + + Hello World + + +`, + object: [ + { + id: '70:51', + name: 'auto-layout-horizon', + type: 'FRAME', + reactions: [], + parent: '70:49', + children: ['70:50', '70:61'], + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'HUG', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3333333432674408, + g: 0, + b: 1, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 129, + height: 46, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + }, + { + id: '70:50', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '70:51', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 87, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '70:61', + name: 'Frame 13', + type: 'FRAME', + visible: true, + parent: '70:51', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12, + height: 12, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + }, + { + id: '70:49', + name: 'Auto-layout & box', + type: 'SECTION', + children: ['70:51'], + }, + ], + }, + { + expected: ` + + Hello World + + +`, + object: [ + { + id: '70:57', + name: 'auto-layout-vertical', + type: 'FRAME', + visible: true, + parent: '70:49', + children: ['70:58', '70:62'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3333333432674408, + g: 0, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 107, + height: 68, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + }, + { + id: '70:58', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '70:57', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 87, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '70:62', + name: 'Frame 13', + type: 'FRAME', + visible: true, + parent: '70:57', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12, + height: 12, + rotation: 0, + cornerRadius: 1000, + topLeftRadius: 1000, + topRightRadius: 1000, + bottomLeftRadius: 1000, + bottomRightRadius: 1000, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + }, + { + id: '70:49', + name: 'Auto-layout & box', + type: 'SECTION', + children: ['70:57'], + }, + ], + }, + { + expected: ` + + Hello World + + +`, + object: [ + { + id: '71:124', + name: 'auto-layout-vertical', + type: 'FRAME', + visible: true, + parent: '70:49', + children: ['71:125', '71:126'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3333333432674408, + g: 0, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 107, + height: 53, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 6, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 6, + layoutPositioning: 'AUTO', + }, + }, + { + id: '71:125', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '71:124', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 87, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '71:126', + name: 'Frame 13', + type: 'FRAME', + visible: true, + parent: '71:124', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 87, + height: 1, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + }, + { + id: '70:49', + name: 'Auto-layout & box', + type: 'SECTION', + children: ['71:124'], + }, + ], + }, + // Component + // { + // expected: ` + // + // 더 자세히 알아보기 + // + // + // `, + // object: [ + // { + // id: '1:5', + // name: 'Button', + // type: 'FRAME', + // reactions: [], + // parent: '71:123', + // children: ['1:6', '2:11'], + // inferredAutoLayout: { + // layoutMode: 'HORIZONTAL', + // paddingLeft: 60, + // paddingRight: 60, + // paddingTop: 12, + // paddingBottom: 12, + // counterAxisSizingMode: 'AUTO', + // primaryAxisSizingMode: 'AUTO', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'CENTER', + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // itemSpacing: 20, + // layoutPositioning: 'AUTO', + // }, + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'CENTER', + // layoutPositioning: 'AUTO', + // layoutSizingVertical: 'HUG', + // layoutSizingHorizontal: 'HUG', + // cornerRadius: 100, + // strokes: [], + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 0.35686275362968445, + // g: 0.20392157137393951, + // b: 0.9686274528503418, + // }, + // boundVariables: { + // color: + // '[NodeId: VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313]', + // }, + // }, + // ], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // isAsset: false, + // effects: [], + // rotation: 0, + // clipsContent: false, + // visible: true, + // width: 287, + // height: 53, + // topLeftRadius: 100, + // topRightRadius: 100, + // bottomLeftRadius: 100, + // bottomRightRadius: 100, + // layoutMode: 'HORIZONTAL', + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // paddingLeft: 60, + // paddingRight: 60, + // paddingTop: 12, + // paddingBottom: 12, + // itemSpacing: 20, + // counterAxisSpacing: 0, + // }, + // { + // id: '1:6', + // name: '더 자세히 알아보기', + // type: 'TEXT', + // visible: true, + // parent: '1:5', + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 1, + // g: 1, + // b: 1, + // }, + // boundVariables: {}, + // }, + // ], + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 127, + // height: 29, + // rotation: 0, + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // layoutSizingHorizontal: 'HUG', + // layoutSizingVertical: 'HUG', + // layoutPositioning: 'AUTO', + // isAsset: false, + // reactions: [], + // characters: '더 자세히 알아보기', + // fontName: { + // family: 'Pretendard', + // style: 'Bold', + // }, + // fontSize: 18, + // fontWeight: 700, + // lineHeight: { + // unit: 'PERCENT', + // value: 160.0000023841858, + // }, + // letterSpacing: { + // unit: 'PERCENT', + // value: -4, + // }, + // textAutoResize: 'WIDTH_AND_HEIGHT', + // textAlignHorizontal: 'LEFT', + // textAlignVertical: 'TOP', + // textTruncation: 'DISABLED', + // }, + // { + // id: '2:11', + // name: 'arrow', + // type: 'FRAME', + // visible: true, + // parent: '1:5', + // children: ['1:7'], + // fills: [], + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 20, + // height: 20, + // rotation: 0, + // cornerRadius: 0, + // topLeftRadius: 0, + // topRightRadius: 0, + // bottomLeftRadius: 0, + // bottomRightRadius: 0, + // layoutMode: 'NONE', + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FIXED', + // layoutSizingVertical: 'FIXED', + // layoutPositioning: 'AUTO', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'MIN', + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // itemSpacing: 0, + // counterAxisSpacing: 0, + // clipsContent: false, + // isAsset: true, + // reactions: [], + // }, + // { + // id: '1:7', + // name: 'Stroke', + // type: 'VECTOR', + // visible: true, + // parent: '2:11', + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 1, + // g: 1, + // b: 1, + // }, + // boundVariables: {}, + // }, + // ], + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 20, + // height: 20, + // rotation: 0, + // cornerRadius: 0, + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FIXED', + // layoutSizingVertical: 'FIXED', + // layoutPositioning: 'AUTO', + // isAsset: false, + // reactions: [], + // targetAspectRatio: { + // x: 24, + // y: 24, + // }, + // }, + // { + // id: '71:123', + // name: 'Component 1', + // type: 'SECTION', + // children: ['1:5'], + // }, + // ], + // }, ] as const)('$expected', async ({ expected, object }) => { const root = assembleNodeTree(object as unknown as NodeData[]) const codegen = new Codegen(root as unknown as SceneNode) From 88c86f68715662a80497f7bab5845c8fc85bb4c6 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 01:53:53 +0900 Subject: [PATCH 12/51] Fix component --- src/code-impl.ts | 4 +- src/codegen/__tests__/codegen.test.ts | 738 ++++++++++++++++++++------ src/codegen/utils/node-proxy.ts | 150 +++++- 3 files changed, 736 insertions(+), 156 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index ae398a7..60d5dc2 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -164,7 +164,9 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } - console.log(nodeProxyTracker.toTestCaseFormat(node.id)) + console.log( + await nodeProxyTracker.toTestCaseFormatWithVariables(node.id), + ) return [ ...(node.type === 'COMPONENT' || diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 2ec30a5..01c90c1 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4356,7 +4356,7 @@ describe('render real world component', () => { it.each([ { expected: ``, - object: [ + nodes: [ { id: '7:3', name: 'Rectangle 2', @@ -4408,7 +4408,7 @@ describe('render real world component', () => { { expected: '', - object: [ + nodes: [ { id: '6:2', name: 'Rectangle 1', @@ -4459,7 +4459,7 @@ describe('render real world component', () => { { expected: '', - object: [ + nodes: [ { id: '7:5', name: 'Rectangle 3', @@ -4520,7 +4520,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2019', name: 'Frame 1597884473', @@ -4693,7 +4693,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2050', name: 'Frame 1597884474', @@ -4866,7 +4866,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2051', name: 'Frame 1597884475', @@ -5039,7 +5039,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2054', name: 'Frame 1597884476', @@ -5212,7 +5212,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2055', name: 'Frame 1597884477', @@ -5385,7 +5385,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2056', name: 'Frame 1597884478', @@ -5558,7 +5558,7 @@ describe('render real world component', () => { > `, - object: [ + nodes: [ { id: '496:2057', name: 'Frame 1597884479', @@ -5731,7 +5731,7 @@ describe('render real world component', () => { > Hello World `, - object: [ + nodes: [ { id: '35:7', name: 'Hello World', @@ -5805,7 +5805,7 @@ describe('render real world component', () => { > Hello World `, - object: [ + nodes: [ { id: '35:12', name: 'Hello World', @@ -5879,7 +5879,7 @@ describe('render real world component', () => { > Hello World `, - object: [ + nodes: [ { id: '35:18', name: 'Hello World', @@ -5954,7 +5954,7 @@ describe('render real world component', () => { > Hello World `, - object: [ + nodes: [ { id: '41:7', name: 'Hello World', @@ -6030,7 +6030,7 @@ describe('render real world component', () => { > Hello World `, - object: [ + nodes: [ { id: '41:12', name: 'Hello World', @@ -6107,7 +6107,7 @@ describe('render real world component', () => { `, - object: [ + nodes: [ { id: '70:51', name: 'auto-layout-horizon', @@ -6301,7 +6301,7 @@ describe('render real world component', () => { `, - object: [ + nodes: [ { id: '70:57', name: 'auto-layout-vertical', @@ -6495,7 +6495,7 @@ describe('render real world component', () => { `, - object: [ + nodes: [ { id: '71:124', name: 'auto-layout-vertical', @@ -6676,63 +6676,292 @@ describe('render real world component', () => { ], }, // Component + { + expected: ` + + 더 자세히 알아보기 + + +`, + nodes: [ + { + id: '1:5', + name: 'Button', + type: 'FRAME', + reactions: [], + parent: '71:123', + children: ['1:6', '2:11'], + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 60, + paddingRight: 60, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'HUG', + cornerRadius: 100, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.35686275362968445, + g: 0.20392157137393951, + b: 0.9686274528503418, + }, + boundVariables: { + color: + '[NodeId: VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313]', + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 287, + height: 53, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingLeft: 60, + paddingRight: 60, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 20, + counterAxisSpacing: 0, + }, + { + id: '1:6', + name: '더 자세히 알아보기', + type: 'TEXT', + visible: true, + parent: '1:5', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 127, + height: 29, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + characters: '더 자세히 알아보기', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 18, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + }, + { + id: '2:11', + name: 'arrow', + type: 'FRAME', + visible: true, + parent: '1:5', + children: ['1:7'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + }, + { + id: '1:7', + name: 'Stroke', + type: 'VECTOR', + visible: true, + parent: '2:11', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + targetAspectRatio: { + x: 24, + y: 24, + }, + }, + { + id: '71:123', + name: 'Component 1', + type: 'SECTION', + children: ['1:5'], + }, + ], + variables: [ + { + id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', + name: 'primary', + }, + { + id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', + name: 'background', + }, + { + id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', + name: 'text', + }, + ], + }, // { - // expected: ` + // expected: ` + // // + // + // 자사 솔루션과의 연계 + // + // 를 통해서비스 확장성과 성장 가능성을 높입니다. + // + // - // 더 자세히 알아보기 + // 웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다. // - // - // `, - // object: [ + // `, + // nodes: [ // { - // id: '1:5', - // name: 'Button', + // id: '1:13', + // name: 'Card', // type: 'FRAME', - // reactions: [], + // visible: true, // parent: '71:123', - // children: ['1:6', '2:11'], - // inferredAutoLayout: { - // layoutMode: 'HORIZONTAL', - // paddingLeft: 60, - // paddingRight: 60, - // paddingTop: 12, - // paddingBottom: 12, - // counterAxisSizingMode: 'AUTO', - // primaryAxisSizingMode: 'AUTO', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'CENTER', - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // itemSpacing: 20, - // layoutPositioning: 'AUTO', - // }, - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'CENTER', - // layoutPositioning: 'AUTO', - // layoutSizingVertical: 'HUG', - // layoutSizingHorizontal: 'HUG', - // cornerRadius: 100, - // strokes: [], + // children: ['1:14', '1:17', '1:18'], // fills: [ // { // type: 'SOLID', @@ -6740,49 +6969,72 @@ describe('render real world component', () => { // opacity: 1, // blendMode: 'NORMAL', // color: { - // r: 0.35686275362968445, - // g: 0.20392157137393951, - // b: 0.9686274528503418, + // r: 0.9697822332382202, + // g: 0.9732044339179993, + // b: 0.9855769276618958, // }, // boundVariables: { // color: - // '[NodeId: VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313]', + // '[NodeId: VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11]', // }, // }, // ], + // strokes: [], + // effects: [], // opacity: 1, // blendMode: 'PASS_THROUGH', - // isAsset: false, - // effects: [], + // width: 420, + // height: 318, // rotation: 0, - // clipsContent: false, - // visible: true, - // width: 287, - // height: 53, - // topLeftRadius: 100, - // topRightRadius: 100, - // bottomLeftRadius: 100, - // bottomRightRadius: 100, - // layoutMode: 'HORIZONTAL', - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // paddingLeft: 60, - // paddingRight: 60, - // paddingTop: 12, - // paddingBottom: 12, + // cornerRadius: 30, + // topLeftRadius: 30, + // topRightRadius: 30, + // bottomLeftRadius: 30, + // bottomRightRadius: 30, + // layoutMode: 'VERTICAL', + // layoutAlign: 'STRETCH', + // layoutGrow: 1, + // layoutSizingHorizontal: 'FIXED', + // layoutSizingVertical: 'HUG', + // layoutPositioning: 'AUTO', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'MIN', + // paddingLeft: 40, + // paddingRight: 40, + // paddingTop: 40, + // paddingBottom: 40, // itemSpacing: 20, // counterAxisSpacing: 0, + // clipsContent: false, + // isAsset: false, + // reactions: [], + // inferredAutoLayout: { + // layoutMode: 'VERTICAL', + // paddingLeft: 40, + // paddingRight: 40, + // paddingTop: 40, + // paddingBottom: 40, + // counterAxisSizingMode: 'FIXED', + // primaryAxisSizingMode: 'AUTO', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'MIN', + // layoutAlign: 'STRETCH', + // layoutGrow: 1, + // itemSpacing: 20, + // layoutPositioning: 'AUTO', + // }, // }, // { - // id: '1:6', - // name: '더 자세히 알아보기', - // type: 'TEXT', + // id: '1:14', + // name: 'puzzle-piece', + // type: 'FRAME', // visible: true, - // parent: '1:5', + // parent: '1:13', + // children: ['1:15', '1:16'], // fills: [ // { // type: 'SOLID', - // visible: true, + // visible: false, // opacity: 1, // blendMode: 'NORMAL', // color: { @@ -6797,50 +7049,8 @@ describe('render real world component', () => { // effects: [], // opacity: 1, // blendMode: 'PASS_THROUGH', - // width: 127, - // height: 29, - // rotation: 0, - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // layoutSizingHorizontal: 'HUG', - // layoutSizingVertical: 'HUG', - // layoutPositioning: 'AUTO', - // isAsset: false, - // reactions: [], - // characters: '더 자세히 알아보기', - // fontName: { - // family: 'Pretendard', - // style: 'Bold', - // }, - // fontSize: 18, - // fontWeight: 700, - // lineHeight: { - // unit: 'PERCENT', - // value: 160.0000023841858, - // }, - // letterSpacing: { - // unit: 'PERCENT', - // value: -4, - // }, - // textAutoResize: 'WIDTH_AND_HEIGHT', - // textAlignHorizontal: 'LEFT', - // textAlignVertical: 'TOP', - // textTruncation: 'DISABLED', - // }, - // { - // id: '2:11', - // name: 'arrow', - // type: 'FRAME', - // visible: true, - // parent: '1:5', - // children: ['1:7'], - // fills: [], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 20, - // height: 20, + // width: 64, + // height: 64, // rotation: 0, // cornerRadius: 0, // topLeftRadius: 0, @@ -6861,16 +7071,16 @@ describe('render real world component', () => { // paddingBottom: 0, // itemSpacing: 0, // counterAxisSpacing: 0, - // clipsContent: false, + // clipsContent: true, // isAsset: true, // reactions: [], // }, // { - // id: '1:7', - // name: 'Stroke', + // id: '1:15', + // name: 'Vector', // type: 'VECTOR', // visible: true, - // parent: '2:11', + // parent: '1:14', // fills: [ // { // type: 'SOLID', @@ -6878,9 +7088,9 @@ describe('render real world component', () => { // opacity: 1, // blendMode: 'NORMAL', // color: { - // r: 1, - // g: 1, - // b: 1, + // r: 0.6901960968971252, + // g: 0.6901960968971252, + // b: 0.686274528503418, // }, // boundVariables: {}, // }, @@ -6889,8 +7099,8 @@ describe('render real world component', () => { // effects: [], // opacity: 1, // blendMode: 'PASS_THROUGH', - // width: 20, - // height: 20, + // width: 57.96072006225586, + // height: 53.89710235595703, // rotation: 0, // cornerRadius: 0, // layoutAlign: 'INHERIT', @@ -6900,21 +7110,249 @@ describe('render real world component', () => { // layoutPositioning: 'AUTO', // isAsset: false, // reactions: [], - // targetAspectRatio: { - // x: 24, - // y: 24, + // }, + // { + // id: '1:16', + // name: 'Vector', + // type: 'VECTOR', + // visible: true, + // parent: '1:14', + // fills: [ + // { + // type: 'GRADIENT_RADIAL', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // gradientStops: [ + // { + // color: { + // r: 0.7176470756530762, + // g: 0.8196078538894653, + // b: 0.0941176488995552, + // a: 1, + // }, + // position: 0.5080000162124634, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.6980392336845398, + // g: 0.8156862854957581, + // b: 0.09803921729326248, + // a: 1, + // }, + // position: 0.5720000267028809, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.6470588445663452, + // g: 0.8039215803146362, + // b: 0.11372549086809158, + // a: 1, + // }, + // position: 0.6430000066757202, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.5607843399047852, + // g: 0.7882353067398071, + // b: 0.13333334028720856, + // a: 1, + // }, + // position: 0.7170000076293945, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.43921568989753723, + // g: 0.7607843279838562, + // b: 0.16470588743686676, + // a: 1, + // }, + // position: 0.7929999828338623, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.2823529541492462, + // g: 0.729411780834198, + // b: 0.20392157137393951, + // a: 1, + // }, + // position: 0.8709999918937683, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.0941176488995552, + // g: 0.6901960968971252, + // b: 0.250980406999588, + // a: 1, + // }, + // position: 0.9490000009536743, + // boundVariables: {}, + // }, + // { + // color: { + // r: 0.007843137718737125, + // g: 0.6705882549285889, + // b: 0.27450981736183167, + // a: 1, + // }, + // position: 0.9810000061988831, + // boundVariables: {}, + // }, + // ], + // gradientTransform: [ + // [0.4799879491329193, 0, 0.26639416813850403], + // [0, 0.48956596851348877, 0.6352904438972473], + // ], + // }, + // ], + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 57.2265625, + // height: 58.36850357055664, + // rotation: 0, + // cornerRadius: 0, + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FIXED', + // layoutSizingVertical: 'FIXED', + // layoutPositioning: 'AUTO', + // isAsset: false, + // reactions: [], + // }, + // { + // id: '1:17', + // name: '자사 솔루션과의 연계를 통해 서비스 확장성과 성장 가능성을 높입니다.', + // type: 'TEXT', + // visible: true, + // parent: '1:13', + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 340, + // height: 56, + // rotation: 0, + // layoutAlign: 'STRETCH', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FILL', + // layoutSizingVertical: 'HUG', + // layoutPositioning: 'AUTO', + // isAsset: false, + // reactions: [], + // characters: + // '자사 솔루션과의 연계를 통해\n서비스 확장성과 성장 가능성을 높입니다.', + // fontName: { + // family: 'Pretendard', + // style: 'ExtraBold', // }, + // fontSize: 20, + // fontWeight: 800, + // lineHeight: { + // unit: 'PERCENT', + // value: 139.9999976158142, + // }, + // letterSpacing: { + // unit: 'PERCENT', + // value: -4, + // }, + // textAutoResize: 'HEIGHT', + // textAlignHorizontal: 'LEFT', + // textAlignVertical: 'TOP', + // textTruncation: 'DISABLED', + // }, + // { + // id: '1:18', + // name: '웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다.', + // type: 'TEXT', + // visible: true, + // parent: '1:13', + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 0.10196078568696976, + // g: 0.10196078568696976, + // b: 0.10196078568696976, + // }, + // boundVariables: { + // color: + // '[NodeId: VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41]', + // }, + // }, + // ], + // strokes: [], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 340, + // height: 78, + // rotation: 0, + // layoutAlign: 'STRETCH', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FILL', + // layoutSizingVertical: 'HUG', + // layoutPositioning: 'AUTO', + // isAsset: false, + // reactions: [], + // characters: + // '웹앱팩토리와 Presskit 등 자체 운영 중인 \n솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, \n운영 효율화, 성장 기회까지 제안드립니다.', + // fontName: { + // family: 'Pretendard', + // style: 'Medium', + // }, + // fontSize: 16, + // fontWeight: 500, + // lineHeight: { + // unit: 'PERCENT', + // value: 160.0000023841858, + // }, + // letterSpacing: { + // unit: 'PERCENT', + // value: -6, + // }, + // textAutoResize: 'HEIGHT', + // textAlignHorizontal: 'LEFT', + // textAlignVertical: 'TOP', + // textTruncation: 'DISABLED', // }, // { // id: '71:123', // name: 'Component 1', // type: 'SECTION', - // children: ['1:5'], + // children: ['1:13'], + // }, + // ], + // variables: [ + // { + // id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', + // name: 'primary', + // }, + // { + // id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', + // name: 'background', + // }, + // { + // id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', + // name: 'text', // }, // ], // }, - ] as const)('$expected', async ({ expected, object }) => { - const root = assembleNodeTree(object as unknown as NodeData[]) + ] as const)('$expected', async ({ expected, nodes, variables }) => { + const root = assembleNodeTree( + nodes as unknown as NodeData[], + variables as { id: string; name: string }[] | undefined, + ) const codegen = new Codegen(root as unknown as SceneNode) await codegen.run() expect(codegen.getCode()).toBe(expected) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index c1b78af..ec546e0 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -265,12 +265,73 @@ class NodeProxyTracker { * Only includes this node and its descendants. */ toTestCaseFormat(rootId?: string): NodeData[] { - // 모든 노드를 먼저 변환 + const allNodes = this.buildAllNodes() + return this.filterNodes(allNodes, rootId) + } + + /** + * Returns nodes and variables info for test cases. + * Variables are extracted from boundVariables in fills/strokes. + */ + async toTestCaseFormatWithVariables(rootId?: string): Promise { + const variableMap = new Map() + + // Variable ID를 수집하는 헬퍼 함수 + const collectVariableIds = (value: unknown): string[] => { + const ids: string[] = [] + if ( + typeof value === 'string' && + value.startsWith('[NodeId: VariableID:') + ) { + const match = value.match(/\[NodeId: (VariableID:[^\]]+)\]/) + if (match) { + ids.push(match[1]) + } + } else if (Array.isArray(value)) { + for (const item of value) { + ids.push(...collectVariableIds(item)) + } + } else if (value && typeof value === 'object') { + for (const v of Object.values(value)) { + ids.push(...collectVariableIds(v)) + } + } + return ids + } + + // 모든 프로퍼티에서 Variable ID 수집 + for (const log of this.accessLogs.values()) { + for (const { value } of log.properties) { + const varIds = collectVariableIds(value) + for (const varId of varIds) { + if (!variableMap.has(varId)) { + try { + const variable = await figma.variables.getVariableByIdAsync(varId) + if (variable?.name) { + variableMap.set(varId, { id: varId, name: variable.name }) + } + } catch { + // ignore - Figma API 없는 환경 + } + } + } + } + } + + const allNodes = this.buildAllNodes() + const resultNodes = this.filterNodes(allNodes, rootId) + + return { + nodes: resultNodes, + variables: Array.from(variableMap.values()), + } + } + + private buildAllNodes(): NodeData[] { const allNodes: NodeData[] = [] for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { - // null 값은 제외 if (value === null) continue props[key] = this.resolveNodeRefs(value) } @@ -281,12 +342,14 @@ class NodeProxyTracker { ...props, }) } + return allNodes + } + private filterNodes(allNodes: NodeData[], rootId?: string): NodeData[] { if (!rootId) { return allNodes } - // rootId가 주어진 경우: 루트 노드와 그 하위 노드들만 필터링 const rootNode = allNodes.find((n) => n.id === rootId) if (!rootNode) { return allNodes @@ -322,7 +385,6 @@ class NodeProxyTracker { if (parentId) { const parentNode = allNodes.find((n) => n.id === parentId) if (parentNode && parentNode.type === 'SECTION') { - // 부모의 children에서 루트 노드만 남기기 parentNode.children = [rootId] result.push(parentNode) } @@ -374,11 +436,61 @@ export type NodeData = Record & { children?: (string | NodeData)[] } +export type VariableInfo = { + id: string + name: string +} + +export type TestCaseData = { + nodes: NodeData[] + variables: VariableInfo[] +} + +/** + * Sets up figma.variables.getVariableByIdAsync mock for test environment. + */ +function setupVariableMocks(variables: VariableInfo[]): void { + if (typeof globalThis === 'undefined') return + + const g = globalThis as { figma?: { variables?: Record } } + if (!g.figma) return + + const variableMap = new Map(variables.map((v) => [v.id, v])) + + // 기존 mock을 보존하면서 새로운 변수들 추가 + const originalGetVariable = g.figma.variables?.getVariableByIdAsync as + | ((id: string) => Promise) + | undefined + + g.figma.variables = { + ...g.figma.variables, + getVariableByIdAsync: async (id: string) => { + const varInfo = variableMap.get(id) + if (varInfo) { + return { id: varInfo.id, name: varInfo.name } + } + // 기존 mock이 있으면 폴백 + if (originalGetVariable) { + return originalGetVariable(id) + } + return null + }, + } +} + /** * Assembles a node tree from a flat list of nodes. * Links parent/children by id references. + * Optionally sets up variable mocks from the variables array. */ -export function assembleNodeTree(nodes: NodeData[]): NodeData { +export function assembleNodeTree( + nodes: NodeData[], + variables?: VariableInfo[], +): NodeData { + // Variable mock 설정 + if (variables && variables.length > 0) { + setupVariableMocks(variables) + } const nodeMap = new Map() // 1. 모든 노드를 복사해서 맵에 저장 @@ -407,6 +519,34 @@ export function assembleNodeTree(nodes: NodeData[]): NodeData { } // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) + // fills/strokes의 boundVariables 처리 + // boundVariables.color가 문자열 ID인 경우 { id: '...' } 객체로 변환 + const processBoundVariables = (paints: unknown[]) => { + for (const paint of paints) { + if (paint && typeof paint === 'object') { + const p = paint as Record + if (p.boundVariables && typeof p.boundVariables === 'object') { + const bv = p.boundVariables as Record + if (typeof bv.color === 'string') { + // '[NodeId: VariableID:...]' 형식에서 실제 ID 추출 + let colorId = bv.color + const match = colorId.match(/\[NodeId: ([^\]]+)\]/) + if (match) { + colorId = match[1] + } + bv.color = { id: colorId } + } + } + } + } + } + if (Array.isArray(node.fills)) { + processBoundVariables(node.fills) + } + if (Array.isArray(node.strokes)) { + processBoundVariables(node.strokes) + } + // TEXT 노드에 getStyledTextSegments mock 메서드 추가 if (node.type === 'TEXT') { const textNode = node as NodeData & { styledTextSegments?: unknown[] } From 0bf1cbb16cc95e5fb5a0b0b2ff2bb0e14f103174 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 02:30:44 +0900 Subject: [PATCH 13/51] Fix component --- src/codegen/__tests__/codegen.test.ts | 1006 ++++++++++++++----------- src/codegen/utils/node-proxy.ts | 67 +- 2 files changed, 651 insertions(+), 422 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 01c90c1..aa517af 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -6927,427 +6927,591 @@ describe('render real world component', () => { }, ], }, - // { - // expected: ` - // - // - // - // 자사 솔루션과의 연계 - // - // 를 통해서비스 확장성과 성장 가능성을 높입니다. - // - // - // 웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다. - // - // `, - // nodes: [ - // { - // id: '1:13', - // name: 'Card', - // type: 'FRAME', - // visible: true, - // parent: '71:123', - // children: ['1:14', '1:17', '1:18'], - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0.9697822332382202, - // g: 0.9732044339179993, - // b: 0.9855769276618958, - // }, - // boundVariables: { - // color: - // '[NodeId: VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11]', - // }, - // }, - // ], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 420, - // height: 318, - // rotation: 0, - // cornerRadius: 30, - // topLeftRadius: 30, - // topRightRadius: 30, - // bottomLeftRadius: 30, - // bottomRightRadius: 30, - // layoutMode: 'VERTICAL', - // layoutAlign: 'STRETCH', - // layoutGrow: 1, - // layoutSizingHorizontal: 'FIXED', - // layoutSizingVertical: 'HUG', - // layoutPositioning: 'AUTO', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'MIN', - // paddingLeft: 40, - // paddingRight: 40, - // paddingTop: 40, - // paddingBottom: 40, - // itemSpacing: 20, - // counterAxisSpacing: 0, - // clipsContent: false, - // isAsset: false, - // reactions: [], - // inferredAutoLayout: { - // layoutMode: 'VERTICAL', - // paddingLeft: 40, - // paddingRight: 40, - // paddingTop: 40, - // paddingBottom: 40, - // counterAxisSizingMode: 'FIXED', - // primaryAxisSizingMode: 'AUTO', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'MIN', - // layoutAlign: 'STRETCH', - // layoutGrow: 1, - // itemSpacing: 20, - // layoutPositioning: 'AUTO', - // }, - // }, - // { - // id: '1:14', - // name: 'puzzle-piece', - // type: 'FRAME', - // visible: true, - // parent: '1:13', - // children: ['1:15', '1:16'], - // fills: [ - // { - // type: 'SOLID', - // visible: false, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 1, - // g: 1, - // b: 1, - // }, - // boundVariables: {}, - // }, - // ], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 64, - // height: 64, - // rotation: 0, - // cornerRadius: 0, - // topLeftRadius: 0, - // topRightRadius: 0, - // bottomLeftRadius: 0, - // bottomRightRadius: 0, - // layoutMode: 'NONE', - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FIXED', - // layoutSizingVertical: 'FIXED', - // layoutPositioning: 'AUTO', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'MIN', - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // itemSpacing: 0, - // counterAxisSpacing: 0, - // clipsContent: true, - // isAsset: true, - // reactions: [], - // }, - // { - // id: '1:15', - // name: 'Vector', - // type: 'VECTOR', - // visible: true, - // parent: '1:14', - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0.6901960968971252, - // g: 0.6901960968971252, - // b: 0.686274528503418, - // }, - // boundVariables: {}, - // }, - // ], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 57.96072006225586, - // height: 53.89710235595703, - // rotation: 0, - // cornerRadius: 0, - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FIXED', - // layoutSizingVertical: 'FIXED', - // layoutPositioning: 'AUTO', - // isAsset: false, - // reactions: [], - // }, - // { - // id: '1:16', - // name: 'Vector', - // type: 'VECTOR', - // visible: true, - // parent: '1:14', - // fills: [ - // { - // type: 'GRADIENT_RADIAL', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // gradientStops: [ - // { - // color: { - // r: 0.7176470756530762, - // g: 0.8196078538894653, - // b: 0.0941176488995552, - // a: 1, - // }, - // position: 0.5080000162124634, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.6980392336845398, - // g: 0.8156862854957581, - // b: 0.09803921729326248, - // a: 1, - // }, - // position: 0.5720000267028809, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.6470588445663452, - // g: 0.8039215803146362, - // b: 0.11372549086809158, - // a: 1, - // }, - // position: 0.6430000066757202, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.5607843399047852, - // g: 0.7882353067398071, - // b: 0.13333334028720856, - // a: 1, - // }, - // position: 0.7170000076293945, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.43921568989753723, - // g: 0.7607843279838562, - // b: 0.16470588743686676, - // a: 1, - // }, - // position: 0.7929999828338623, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.2823529541492462, - // g: 0.729411780834198, - // b: 0.20392157137393951, - // a: 1, - // }, - // position: 0.8709999918937683, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.0941176488995552, - // g: 0.6901960968971252, - // b: 0.250980406999588, - // a: 1, - // }, - // position: 0.9490000009536743, - // boundVariables: {}, - // }, - // { - // color: { - // r: 0.007843137718737125, - // g: 0.6705882549285889, - // b: 0.27450981736183167, - // a: 1, - // }, - // position: 0.9810000061988831, - // boundVariables: {}, - // }, - // ], - // gradientTransform: [ - // [0.4799879491329193, 0, 0.26639416813850403], - // [0, 0.48956596851348877, 0.6352904438972473], - // ], - // }, - // ], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 57.2265625, - // height: 58.36850357055664, - // rotation: 0, - // cornerRadius: 0, - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FIXED', - // layoutSizingVertical: 'FIXED', - // layoutPositioning: 'AUTO', - // isAsset: false, - // reactions: [], - // }, - // { - // id: '1:17', - // name: '자사 솔루션과의 연계를 통해 서비스 확장성과 성장 가능성을 높입니다.', - // type: 'TEXT', - // visible: true, - // parent: '1:13', - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 340, - // height: 56, - // rotation: 0, - // layoutAlign: 'STRETCH', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FILL', - // layoutSizingVertical: 'HUG', - // layoutPositioning: 'AUTO', - // isAsset: false, - // reactions: [], - // characters: - // '자사 솔루션과의 연계를 통해\n서비스 확장성과 성장 가능성을 높입니다.', - // fontName: { - // family: 'Pretendard', - // style: 'ExtraBold', - // }, - // fontSize: 20, - // fontWeight: 800, - // lineHeight: { - // unit: 'PERCENT', - // value: 139.9999976158142, - // }, - // letterSpacing: { - // unit: 'PERCENT', - // value: -4, - // }, - // textAutoResize: 'HEIGHT', - // textAlignHorizontal: 'LEFT', - // textAlignVertical: 'TOP', - // textTruncation: 'DISABLED', - // }, - // { - // id: '1:18', - // name: '웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다.', - // type: 'TEXT', - // visible: true, - // parent: '1:13', - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0.10196078568696976, - // g: 0.10196078568696976, - // b: 0.10196078568696976, - // }, - // boundVariables: { - // color: - // '[NodeId: VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41]', - // }, - // }, - // ], - // strokes: [], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 340, - // height: 78, - // rotation: 0, - // layoutAlign: 'STRETCH', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FILL', - // layoutSizingVertical: 'HUG', - // layoutPositioning: 'AUTO', - // isAsset: false, - // reactions: [], - // characters: - // '웹앱팩토리와 Presskit 등 자체 운영 중인 \n솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, \n운영 효율화, 성장 기회까지 제안드립니다.', - // fontName: { - // family: 'Pretendard', - // style: 'Medium', - // }, - // fontSize: 16, - // fontWeight: 500, - // lineHeight: { - // unit: 'PERCENT', - // value: 160.0000023841858, - // }, - // letterSpacing: { - // unit: 'PERCENT', - // value: -6, - // }, - // textAutoResize: 'HEIGHT', - // textAlignHorizontal: 'LEFT', - // textAlignVertical: 'TOP', - // textTruncation: 'DISABLED', - // }, - // { - // id: '71:123', - // name: 'Component 1', - // type: 'SECTION', - // children: ['1:13'], - // }, - // ], - // variables: [ - // { - // id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', - // name: 'primary', - // }, - // { - // id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', - // name: 'background', - // }, - // { - // id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', - // name: 'text', - // }, - // ], - // }, + { + expected: ` + + + + 자사 솔루션과의 연계 + + 를 통해서비스 확장성과 성장 가능성을 높입니다. + + + 웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다. + +`, + nodes: [ + { + id: '1:13', + name: 'Card', + type: 'FRAME', + reactions: [], + parent: '71:123', + children: ['1:14', '1:17', '1:18'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 30, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9697822332382202, + g: 0.9732044339179993, + b: 0.9855769276618958, + }, + boundVariables: { + color: + '[NodeId: VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11]', + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + layoutMode: 'VERTICAL', + width: 420, + height: 318, + topLeftRadius: 30, + topRightRadius: 30, + bottomLeftRadius: 30, + bottomRightRadius: 30, + layoutAlign: 'STRETCH', + layoutGrow: 1, + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + itemSpacing: 20, + counterAxisSpacing: 0, + }, + { + id: '1:14', + name: 'puzzle-piece', + type: 'FRAME', + visible: true, + parent: '1:13', + children: ['1:15', '1:16'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 64, + height: 64, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + }, + { + id: '1:15', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '1:14', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6901960968971252, + g: 0.6901960968971252, + b: 0.686274528503418, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 57.96072006225586, + height: 53.89710235595703, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + }, + { + id: '1:16', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '1:14', + fills: [ + { + type: 'GRADIENT_RADIAL', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.7176470756530762, + g: 0.8196078538894653, + b: 0.0941176488995552, + a: 1, + }, + position: 0.5080000162124634, + boundVariables: {}, + }, + { + color: { + r: 0.6980392336845398, + g: 0.8156862854957581, + b: 0.09803921729326248, + a: 1, + }, + position: 0.5720000267028809, + boundVariables: {}, + }, + { + color: { + r: 0.6470588445663452, + g: 0.8039215803146362, + b: 0.11372549086809158, + a: 1, + }, + position: 0.6430000066757202, + boundVariables: {}, + }, + { + color: { + r: 0.5607843399047852, + g: 0.7882353067398071, + b: 0.13333334028720856, + a: 1, + }, + position: 0.7170000076293945, + boundVariables: {}, + }, + { + color: { + r: 0.43921568989753723, + g: 0.7607843279838562, + b: 0.16470588743686676, + a: 1, + }, + position: 0.7929999828338623, + boundVariables: {}, + }, + { + color: { + r: 0.2823529541492462, + g: 0.729411780834198, + b: 0.20392157137393951, + a: 1, + }, + position: 0.8709999918937683, + boundVariables: {}, + }, + { + color: { + r: 0.0941176488995552, + g: 0.6901960968971252, + b: 0.250980406999588, + a: 1, + }, + position: 0.9490000009536743, + boundVariables: {}, + }, + { + color: { + r: 0.007843137718737125, + g: 0.6705882549285889, + b: 0.27450981736183167, + a: 1, + }, + position: 0.9810000061988831, + boundVariables: {}, + }, + ], + gradientTransform: [ + [0.4799879491329193, 0, 0.26639416813850403], + [0, 0.48956596851348877, 0.6352904438972473], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 57.2265625, + height: 58.36850357055664, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + }, + { + id: '1:17', + name: '자사 솔루션과의 연계를 통해 서비스 확장성과 성장 가능성을 높입니다.', + type: 'TEXT', + visible: true, + parent: '1:13', + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 340, + height: 56, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: + '자사 솔루션과의 연계를 통해\n서비스 확장성과 성장 가능성을 높입니다.', + fontName: { + family: 'Pretendard', + style: 'ExtraBold', + }, + fontSize: 20, + fontWeight: 800, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '자사 솔루션과의 연계', + start: 0, + end: 11, + fontSize: 20, + fontName: { + family: 'Pretendard', + style: 'ExtraBold', + }, + fontWeight: 800, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.35686275362968445, + g: 0.20392157137393951, + b: 0.9686274528503418, + }, + boundVariables: { + color: + '[NodeId: VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313]', + }, + }, + ], + textStyleId: 'S:42aa610b2125d507c89cd19b737e1c07835c76cd,4987:43', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + { + characters: '를 통해\n서비스 확장성과 성장 가능성을 높입니다.', + start: 11, + end: 38, + fontSize: 20, + fontName: { + family: 'Pretendard', + style: 'ExtraBold', + }, + fontWeight: 800, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: { + color: + '[NodeId: VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41]', + }, + }, + ], + textStyleId: 'S:42aa610b2125d507c89cd19b737e1c07835c76cd,4987:43', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '1:18', + name: '웹앱팩토리와 Presskit 등 자체 운영 중인 솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, 운영 효율화, 성장 기회까지 제안드립니다.', + type: 'TEXT', + visible: true, + parent: '1:13', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: { + color: + '[NodeId: VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 340, + height: 78, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: + '웹앱팩토리와 Presskit 등 자체 운영 중인 \n솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, \n운영 효율화, 성장 기회까지 제안드립니다.', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 16, + fontWeight: 500, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + '웹앱팩토리와 Presskit 등 자체 운영 중인 \n솔루션과의 연계를 통해 프로젝트의 서비스 범위 확장, \n운영 효율화, 성장 기회까지 제안드립니다.', + start: 0, + end: 82, + fontSize: 16, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: { + color: + '[NodeId: VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41]', + }, + }, + ], + textStyleId: 'S:f416ab13d47ad55393b3cf3c7e2a45e33c6eec40,4987:46', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '71:123', + name: 'Component 1', + type: 'SECTION', + children: ['1:13'], + }, + ], + variables: [ + { + id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', + name: 'background', + }, + { + id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', + name: 'primary', + }, + { + id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', + name: 'text', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index ec546e0..6904e6e 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -186,6 +186,49 @@ class NodeProxyTracker { // ignore } } + + // TEXT 노드인 경우 getStyledTextSegments 호출해서 결과 저장 + if (node.type === 'TEXT') { + try { + const textNode = node as TextNode + const SEGMENT_TYPE = [ + 'fontName', + 'fontWeight', + 'fontSize', + 'textDecoration', + 'textCase', + 'lineHeight', + 'letterSpacing', + 'fills', + 'textStyleId', + 'fillStyleId', + 'listOptions', + 'indentation', + 'hyperlink', + ] as const + const segments = textNode.getStyledTextSegments( + SEGMENT_TYPE as unknown as (keyof Omit< + StyledTextSegment, + 'characters' | 'start' | 'end' + >)[], + ) + // 세그먼트 직렬화 + const serializedSegments = segments.map((seg) => ({ + ...seg, + fills: this.serializeArray(seg.fills as unknown[]), + })) + + const log = this.accessLogs.get(node.id) + if (log) { + log.properties.push({ + key: 'styledTextSegments', + value: serializedSegments, + }) + } + } catch { + // ignore - getStyledTextSegments가 없는 환경 + } + } } private serializeArray(arr: unknown[]): unknown[] { @@ -328,11 +371,20 @@ class NodeProxyTracker { } private buildAllNodes(): NodeData[] { + // null이어도 포함되어야 하는 프로퍼티들 + const nullableProps = new Set([ + 'maxWidth', + 'maxHeight', + 'minWidth', + 'minHeight', + ]) + const allNodes: NodeData[] = [] for (const log of this.accessLogs.values()) { const props: Record = {} for (const { key, value } of log.properties) { - if (value === null) continue + // null 값은 nullableProps에 포함된 경우만 유지 + if (value === null && !nullableProps.has(key)) continue props[key] = this.resolveNodeRefs(value) } allNodes.push({ @@ -550,6 +602,19 @@ export function assembleNodeTree( // TEXT 노드에 getStyledTextSegments mock 메서드 추가 if (node.type === 'TEXT') { const textNode = node as NodeData & { styledTextSegments?: unknown[] } + + // styledTextSegments 내의 fills도 boundVariables 처리 + if (textNode.styledTextSegments) { + for (const seg of textNode.styledTextSegments) { + if (seg && typeof seg === 'object') { + const s = seg as Record + if (Array.isArray(s.fills)) { + processBoundVariables(s.fills) + } + } + } + } + ;(node as unknown as Record).getStyledTextSegments = () => { // 테스트 데이터에 styledTextSegments가 있으면 사용 From 1e26c0d76164cb0de85201dd76abaea9df0ee061 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 02:41:38 +0900 Subject: [PATCH 14/51] Fix component --- src/codegen/__tests__/codegen.test.ts | 168 +++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 14 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index aa517af..a665e0f 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -4381,6 +4381,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, width: 150, height: 150, cornerRadius: 20, @@ -4433,6 +4437,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, width: 150, height: 150, topLeftRadius: 20, @@ -4495,6 +4503,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -4566,6 +4578,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, height: 223, cornerRadius: 0, topLeftRadius: 0, @@ -4647,6 +4663,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, width: 150, height: 150, cornerRadius: 1000, @@ -4748,6 +4768,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -4820,6 +4844,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, width: 150, height: 150, cornerRadius: 1000, @@ -4912,6 +4940,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, height: 254, cornerRadius: 0, topLeftRadius: 0, @@ -4991,6 +5023,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5078,6 +5114,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5164,6 +5204,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5251,6 +5295,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5337,6 +5385,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5424,6 +5476,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5510,6 +5566,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5597,6 +5657,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5683,6 +5747,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -5740,6 +5808,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'FIXED', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, textAutoResize: 'WIDTH_AND_HEIGHT', strokes: [], fills: [ @@ -5837,6 +5909,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -5911,6 +5987,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -5986,6 +6066,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6062,6 +6146,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6135,6 +6223,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'HUG', layoutSizingHorizontal: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, cornerRadius: 0, topLeftRadius: 0, topRightRadius: 0, @@ -6205,6 +6297,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6266,6 +6362,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -6340,6 +6440,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'CENTER', @@ -6399,6 +6503,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6460,6 +6568,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -6534,6 +6646,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'CENTER', @@ -6593,6 +6709,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6654,6 +6774,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FILL', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -6731,6 +6855,10 @@ describe('render real world component', () => { layoutPositioning: 'AUTO', layoutSizingVertical: 'HUG', layoutSizingHorizontal: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, cornerRadius: 100, strokes: [], fills: [ @@ -6804,6 +6932,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'HUG', layoutSizingVertical: 'HUG', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6852,6 +6984,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -6897,6 +7033,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', isAsset: false, reactions: [], @@ -6979,13 +7119,13 @@ describe('render real world component', () => { }, primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', - maxWidth: null, - maxHeight: null, - minWidth: null, - minHeight: null, layoutPositioning: 'AUTO', layoutSizingVertical: 'HUG', layoutSizingHorizontal: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, cornerRadius: 30, strokes: [], fills: [ @@ -7066,6 +7206,10 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, layoutPositioning: 'AUTO', primaryAxisAlignItems: 'MIN', counterAxisAlignItems: 'MIN', @@ -7078,10 +7222,6 @@ describe('render real world component', () => { clipsContent: true, isAsset: true, reactions: [], - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null, }, { id: '1:15', @@ -7115,13 +7255,13 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', - layoutPositioning: 'AUTO', - isAsset: false, - reactions: [], minWidth: null, maxWidth: null, minHeight: null, maxHeight: null, + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], }, { id: '1:16', @@ -7235,13 +7375,13 @@ describe('render real world component', () => { layoutGrow: 0, layoutSizingHorizontal: 'FIXED', layoutSizingVertical: 'FIXED', - layoutPositioning: 'AUTO', - isAsset: false, - reactions: [], minWidth: null, maxWidth: null, minHeight: null, maxHeight: null, + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], }, { id: '1:17', From 8476bcda0ac1edb17a19615b7848ef5875d9503a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 02:44:14 +0900 Subject: [PATCH 15/51] Fix component --- src/codegen/__tests__/codegen.test.ts | 1074 +++++++++++++++++++++++++ 1 file changed, 1074 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index a665e0f..7f354cd 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -7652,6 +7652,1080 @@ describe('render real world component', () => { }, ], }, + { + expected: ` +
+ + CARD 1 + +
+
+ + CARD 2 + +
+
+ + CARD 3 + +
+
`, + + nodes: [ + { + id: '43:20', + name: 'CardContainer', + type: 'FRAME', + visible: true, + parent: '71:123', + children: ['43:2', '43:21', '43:23'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1200, + height: 280, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 20, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + }, + { + id: '43:2', + name: 'Card', + type: 'FRAME', + visible: true, + parent: '43:20', + children: ['43:7'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 386.6666564941406, + height: 280, + rotation: 0, + cornerRadius: 30, + topLeftRadius: 30, + topRightRadius: 30, + bottomLeftRadius: 30, + bottomRightRadius: 30, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + itemSpacing: 20, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + }, + { + id: '43:7', + name: 'CARD 1', + type: 'TEXT', + visible: true, + parent: '43:2', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 58, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: 'CARD 1', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 36, + fontWeight: 500, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'CARD 1', + start: 0, + end: 6, + fontSize: 36, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '43:21', + name: 'Card', + type: 'FRAME', + visible: true, + parent: '43:20', + children: ['43:22'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 386.66668701171875, + height: 280, + rotation: 0, + cornerRadius: 30, + topLeftRadius: 30, + topRightRadius: 30, + bottomLeftRadius: 30, + bottomRightRadius: 30, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + itemSpacing: 20, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + }, + { + id: '43:22', + name: 'CARD 2', + type: 'TEXT', + visible: true, + parent: '43:21', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 116, + height: 58, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: 'CARD 2', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 36, + fontWeight: 500, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'CARD 2', + start: 0, + end: 6, + fontSize: 36, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '43:23', + name: 'Card', + type: 'FRAME', + visible: true, + parent: '43:20', + children: ['43:24'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 386.66668701171875, + height: 280, + rotation: 0, + cornerRadius: 30, + topLeftRadius: 30, + topRightRadius: 30, + bottomLeftRadius: 30, + bottomRightRadius: 30, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + itemSpacing: 20, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 40, + paddingBottom: 40, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + }, + { + id: '43:24', + name: 'CARD 3', + type: 'TEXT', + visible: true, + parent: '43:23', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 117, + height: 58, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: 'CARD 3', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 36, + fontWeight: 500, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'CARD 3', + start: 0, + end: 6, + fontSize: 36, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -6, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.10196078568696976, + g: 0.10196078568696976, + b: 0.10196078568696976, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '71:123', + name: 'Component 1', + type: 'SECTION', + children: ['43:20'], + }, + ], + variables: [ + { + id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', + name: 'background', + }, + { + id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', + name: 'primary', + }, + { + id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', + name: 'text', + }, + ], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet,{" "} + + consectetur + + {" "}adipiscing elit. + +`, + + nodes: [ + { + id: '40:51', + name: 'font variable', + type: 'FRAME', + visible: true, + parent: '71:123', + children: ['40:52', '40:53'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0.05999999865889549, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [ + { + type: 'BACKGROUND_BLUR', + visible: true, + radius: 4, + boundVariables: {}, + blurType: 'NORMAL', + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 74, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MAX', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MAX', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + }, + { + id: '40:52', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:51', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 134, + height: 29, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Semi Bold Italic', + }, + fontSize: 24, + fontWeight: 600, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'RIGHT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 24, + fontName: { + family: 'Inter', + style: 'Semi Bold Italic', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:53', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:51', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 320, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontSize: 12, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'RIGHT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Lorem ipsum dolor sit amet, ', + start: 0, + end: 28, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + { + characters: 'consectetur', + start: 28, + end: 39, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Black', + }, + fontWeight: 900, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + { + characters: ' adipiscing elit.', + start: 39, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '71:123', + name: 'Component 1', + type: 'SECTION', + children: ['40:51'], + }, + ], + variables: [ + { + id: 'VariableID:15f2b5c6b66588df2b6463e5084ce0334621dcd6/3584:11', + name: 'background', + }, + { + id: 'VariableID:0b96ad7095bac52695a42f130ba1e6823e711569/3589:313', + name: 'primary', + }, + { + id: 'VariableID:a8911963a3ddc27e66ce960494a4683d9c4b1cab/1851:41', + name: 'text', + }, + ], + }, + // outline, border ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From d6eb8e7785af06dd20aa85cdbc07fc2600e1bb47 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 28 Dec 2025 22:51:20 +0900 Subject: [PATCH 16/51] Add case --- src/codegen/__tests__/codegen.test.ts | 1337 +++++++++++++++++++++++++ 1 file changed, 1337 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 7f354cd..0072771 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -8726,6 +8726,1343 @@ describe('render real world component', () => { ], }, // outline, border + { + expected: ``, + nodes: [ + { + id: '105:11', + name: 'inside', + type: 'FRAME', + reactions: [], + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'INSIDE', + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:11'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:64', + name: 'center', + type: 'FRAME', + reactions: [], + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'CENTER', + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:64'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:12', + name: 'outside', + type: 'FRAME', + visible: true, + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'OUTSIDE', + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:12'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:75', + name: 'inside', + type: 'FRAME', + visible: true, + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'INSIDE', + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:75'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:77', + name: 'center', + type: 'FRAME', + visible: true, + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'CENTER', + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:77'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:90', + name: 'outside', + type: 'FRAME', + reactions: [], + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 20, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'OUTSIDE', + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['105:90'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '113:67', + name: 'inside + BoxShadow', + type: 'FRAME', + visible: true, + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.25, + }, + offset: { + x: 0, + y: 4, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'INSIDE', + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['113:67'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '113:71', + name: 'outside + BoxShadow', + type: 'FRAME', + visible: true, + parent: '105:14', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.11057692021131516, + b: 0.11057692021131516, + }, + boundVariables: {}, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.25, + }, + offset: { + x: 0, + y: 4, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [], + strokeWeight: 3, + strokeAlign: 'OUTSIDE', + }, + { + id: '105:14', + name: 'outline, border', + type: 'SECTION', + children: ['113:71'], + }, + ], + variables: [], + }, + // circle + { + expected: ``, + nodes: [ + { + id: '109:69', + name: 'Ellipse 3620', + type: 'ELLIPSE', + reactions: [], + parent: '109:83', + arcData: { + startingAngle: 0, + endingAngle: 6.2831854820251465, + innerRadius: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + isAsset: true, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + width: 100, + height: 100, + cornerRadius: 0, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + }, + { + id: '109:83', + name: 'Circle', + type: 'SECTION', + children: ['109:69'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '109:72', + name: 'Ellipse 3622', + type: 'ELLIPSE', + visible: true, + parent: '109:83', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 50, + height: 100, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + arcData: { + startingAngle: 0, + endingAngle: 6.2831854820251465, + innerRadius: 0, + }, + }, + { + id: '109:83', + name: 'Circle', + type: 'SECTION', + children: ['109:72'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '109:71', + name: 'ellipse3621', + type: 'ELLIPSE', + visible: true, + parent: '109:83', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + arcData: { + startingAngle: 0, + endingAngle: 5.6181230545043945, + innerRadius: 0.4148121774196625, + }, + }, + { + id: '109:83', + name: 'Circle', + type: 'SECTION', + children: ['109:71'], + }, + ], + variables: [], + }, + // border + { + expected: ``, + nodes: [ + { + id: '80:2', + name: 'border', + type: 'FRAME', + reactions: [], + parent: '35:3', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 20, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 1, + strokeAlign: 'CENTER', + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + }, + { + id: '35:3', + name: 'Border', + type: 'SECTION', + children: ['80:2'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '80:6', + name: 'border', + type: 'FRAME', + visible: true, + parent: '35:3', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + dashPattern: [2, 2], + strokeWeight: 1, + strokeAlign: 'CENTER', + }, + { + id: '35:3', + name: 'Border', + type: 'SECTION', + children: ['80:6'], + }, + ], + variables: [], + }, + // { + // expected: ``, + // nodes: [ + // { + // id: '80:40', + // name: 'border', + // type: 'FRAME', + // reactions: [], + // inferredAutoLayout: { + // layoutMode: 'HORIZONTAL', + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // counterAxisSizingMode: 'AUTO', + // primaryAxisSizingMode: 'FIXED', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'CENTER', + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // itemSpacing: 0, + // layoutPositioning: 'AUTO', + // }, + // parent: '35:3', + // layoutMode: 'HORIZONTAL', + // children: ['80:38', '80:39'], + // maxWidth: null, + // maxHeight: null, + // minWidth: null, + // minHeight: null, + // layoutPositioning: 'AUTO', + // layoutSizingVertical: 'HUG', + // layoutSizingHorizontal: 'FIXED', + // cornerRadius: 0, + // topLeftRadius: 0, + // topRightRadius: 0, + // bottomRightRadius: 0, + // bottomLeftRadius: 0, + // strokes: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 0, + // g: 0, + // b: 0, + // }, + // boundVariables: {}, + // }, + // ], + // dashPattern: [], + // strokeBottomWeight: 0, + // strokeTopWeight: 1, + // strokeLeftWeight: 0, + // strokeRightWeight: 0, + // fills: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // isAsset: false, + // effects: [], + // rotation: 0, + // clipsContent: false, + // visible: true, + // width: 260, + // height: 140, + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'CENTER', + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // itemSpacing: 0, + // counterAxisSpacing: 0, + // }, + // { + // id: '80:39', + // name: 'border', + // type: 'FRAME', + // reactions: [], + // parent: '80:40', + // children: [], + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 1, + // g: 1, + // b: 1, + // }, + // boundVariables: {}, + // }, + // ], + // isAsset: false, + // maxWidth: null, + // maxHeight: null, + // minWidth: null, + // minHeight: null, + // layoutPositioning: 'AUTO', + // layoutSizingVertical: 'FIXED', + // layoutSizingHorizontal: 'FILL', + // height: 140, + // cornerRadius: 0, + // topLeftRadius: 0, + // topRightRadius: 0, + // bottomRightRadius: 0, + // bottomLeftRadius: 0, + // strokes: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 0, + // g: 0, + // b: 0, + // }, + // boundVariables: {}, + // }, + // ], + // dashPattern: [], + // strokeBottomWeight: 1, + // strokeTopWeight: 0, + // strokeLeftWeight: 1, + // strokeRightWeight: 1, + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // paddingTop: 0, + // paddingRight: 0, + // paddingBottom: 0, + // paddingLeft: 0, + // effects: [], + // rotation: 0, + // clipsContent: true, + // visible: true, + // width: 180, + // layoutMode: 'NONE', + // layoutAlign: 'INHERIT', + // layoutGrow: 1, + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'MIN', + // itemSpacing: 0, + // counterAxisSpacing: 0, + // }, + // { + // id: '80:38', + // name: 'border', + // type: 'FRAME', + // visible: true, + // parent: '80:40', + // children: [], + // fills: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 1, + // g: 1, + // b: 1, + // }, + // boundVariables: {}, + // }, + // ], + // strokes: [ + // { + // type: 'SOLID', + // visible: true, + // opacity: 1, + // blendMode: 'NORMAL', + // color: { + // r: 0, + // g: 0, + // b: 0, + // }, + // boundVariables: {}, + // }, + // ], + // effects: [], + // opacity: 1, + // blendMode: 'PASS_THROUGH', + // width: 80, + // height: 140, + // rotation: 0, + // cornerRadius: 0, + // topLeftRadius: 0, + // topRightRadius: 0, + // bottomLeftRadius: 0, + // bottomRightRadius: 0, + // layoutMode: 'NONE', + // layoutAlign: 'INHERIT', + // layoutGrow: 0, + // layoutSizingHorizontal: 'FIXED', + // layoutSizingVertical: 'FIXED', + // layoutPositioning: 'AUTO', + // primaryAxisAlignItems: 'MIN', + // counterAxisAlignItems: 'MIN', + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // itemSpacing: 0, + // counterAxisSpacing: 0, + // clipsContent: true, + // isAsset: false, + // reactions: [], + // minWidth: null, + // maxWidth: null, + // minHeight: null, + // maxHeight: null, + // }, + // { + // id: '35:3', + // name: 'Border', + // type: 'SECTION', + // children: ['80:40'], + // }, + // ], + // variables: [], + // }, + // opacity ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 91847b48e1c5fd0d51a0f381d70adc2acd5b8908 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 28 Dec 2025 23:20:48 +0900 Subject: [PATCH 17/51] Add effect test case --- src/codegen/__tests__/codegen.test.ts | 1675 +++++++++++++++++++++---- src/codegen/utils/node-proxy.ts | 25 + 2 files changed, 1466 insertions(+), 234 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 0072771..d0e6f9f 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -9828,241 +9828,1448 @@ describe('render real world component', () => { ], variables: [], }, - // { - // expected: ``, - // nodes: [ - // { - // id: '80:40', - // name: 'border', - // type: 'FRAME', - // reactions: [], - // inferredAutoLayout: { - // layoutMode: 'HORIZONTAL', - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // counterAxisSizingMode: 'AUTO', - // primaryAxisSizingMode: 'FIXED', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'CENTER', - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // itemSpacing: 0, - // layoutPositioning: 'AUTO', - // }, - // parent: '35:3', - // layoutMode: 'HORIZONTAL', - // children: ['80:38', '80:39'], - // maxWidth: null, - // maxHeight: null, - // minWidth: null, - // minHeight: null, - // layoutPositioning: 'AUTO', - // layoutSizingVertical: 'HUG', - // layoutSizingHorizontal: 'FIXED', - // cornerRadius: 0, - // topLeftRadius: 0, - // topRightRadius: 0, - // bottomRightRadius: 0, - // bottomLeftRadius: 0, - // strokes: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0, - // g: 0, - // b: 0, - // }, - // boundVariables: {}, - // }, - // ], - // dashPattern: [], - // strokeBottomWeight: 0, - // strokeTopWeight: 1, - // strokeLeftWeight: 0, - // strokeRightWeight: 0, - // fills: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // isAsset: false, - // effects: [], - // rotation: 0, - // clipsContent: false, - // visible: true, - // width: 260, - // height: 140, - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'CENTER', - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // itemSpacing: 0, - // counterAxisSpacing: 0, - // }, - // { - // id: '80:39', - // name: 'border', - // type: 'FRAME', - // reactions: [], - // parent: '80:40', - // children: [], - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 1, - // g: 1, - // b: 1, - // }, - // boundVariables: {}, - // }, - // ], - // isAsset: false, - // maxWidth: null, - // maxHeight: null, - // minWidth: null, - // minHeight: null, - // layoutPositioning: 'AUTO', - // layoutSizingVertical: 'FIXED', - // layoutSizingHorizontal: 'FILL', - // height: 140, - // cornerRadius: 0, - // topLeftRadius: 0, - // topRightRadius: 0, - // bottomRightRadius: 0, - // bottomLeftRadius: 0, - // strokes: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0, - // g: 0, - // b: 0, - // }, - // boundVariables: {}, - // }, - // ], - // dashPattern: [], - // strokeBottomWeight: 1, - // strokeTopWeight: 0, - // strokeLeftWeight: 1, - // strokeRightWeight: 1, - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // paddingTop: 0, - // paddingRight: 0, - // paddingBottom: 0, - // paddingLeft: 0, - // effects: [], - // rotation: 0, - // clipsContent: true, - // visible: true, - // width: 180, - // layoutMode: 'NONE', - // layoutAlign: 'INHERIT', - // layoutGrow: 1, - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'MIN', - // itemSpacing: 0, - // counterAxisSpacing: 0, - // }, - // { - // id: '80:38', - // name: 'border', - // type: 'FRAME', - // visible: true, - // parent: '80:40', - // children: [], - // fills: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 1, - // g: 1, - // b: 1, - // }, - // boundVariables: {}, - // }, - // ], - // strokes: [ - // { - // type: 'SOLID', - // visible: true, - // opacity: 1, - // blendMode: 'NORMAL', - // color: { - // r: 0, - // g: 0, - // b: 0, - // }, - // boundVariables: {}, - // }, - // ], - // effects: [], - // opacity: 1, - // blendMode: 'PASS_THROUGH', - // width: 80, - // height: 140, - // rotation: 0, - // cornerRadius: 0, - // topLeftRadius: 0, - // topRightRadius: 0, - // bottomLeftRadius: 0, - // bottomRightRadius: 0, - // layoutMode: 'NONE', - // layoutAlign: 'INHERIT', - // layoutGrow: 0, - // layoutSizingHorizontal: 'FIXED', - // layoutSizingVertical: 'FIXED', - // layoutPositioning: 'AUTO', - // primaryAxisAlignItems: 'MIN', - // counterAxisAlignItems: 'MIN', - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // itemSpacing: 0, - // counterAxisSpacing: 0, - // clipsContent: true, - // isAsset: false, - // reactions: [], - // minWidth: null, - // maxWidth: null, - // minHeight: null, - // maxHeight: null, - // }, - // { - // id: '35:3', - // name: 'Border', - // type: 'SECTION', - // children: ['80:40'], - // }, - // ], - // variables: [], - // }, + { + expected: ``, + nodes: [ + { + id: '80:40', + name: 'border', + type: 'FRAME', + reactions: [], + parent: '35:3', + children: ['80:38', '80:39'], + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeBottomWeight: 0, + strokeTopWeight: 1, + strokeLeftWeight: 0, + strokeRightWeight: 0, + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 260, + height: 140, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + strokeAlign: 'OUTSIDE', + }, + { + id: '80:38', + name: 'border', + type: 'FRAME', + visible: true, + parent: '80:40', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 80, + height: 140, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeTopWeight: 0, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '80:39', + name: 'border', + type: 'FRAME', + visible: true, + parent: '80:40', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 180, + height: 140, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeTopWeight: 0, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '35:3', + name: 'Border', + type: 'SECTION', + children: ['80:40'], + }, + ], + variables: [], + }, // opacity + { + expected: ``, + nodes: [ + { + id: '89:3', + name: 'opacity', + type: 'FRAME', + reactions: [], + parent: '89:2', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 20, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 1, + strokeAlign: 'INSIDE', + opacity: 0.20000000298023224, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + }, + { + id: '89:2', + name: 'Opacity', + type: 'SECTION', + children: ['89:3'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:91', + name: 'opacity', + type: 'FRAME', + visible: true, + parent: '89:2', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0.20000000298023224, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 0.20000000298023224, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '89:2', + name: 'Opacity', + type: 'SECTION', + children: ['105:91'], + }, + ], + variables: [], + }, + // clamp + { + expected: ` + + Getting into life {"'"}Cause I found that it{"'"}s not so boring Not anymore. + +`, + nodes: [ + { + id: '70:72', + name: 'clamp - fill', + type: 'FRAME', + reactions: [], + parent: '60:26', + children: ['70:73'], + paddingLeft: 10, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + layoutMode: 'HORIZONTAL', + width: 600, + height: 46, + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '70:73', + name: "Getting into life 'Cause I found that it's not so boring Not anymore.", + type: 'TEXT', + visible: true, + parent: '70:72', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 580, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'ENDING', + maxLines: 1, + styledTextSegments: [ + { + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + start: 0, + end: 69, + fontSize: 16, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:26', + name: 'clamp', + type: 'SECTION', + children: ['70:72'], + }, + ], + variables: [], + }, + { + expected: ` + + Getting into life {"'"}Cause I found that it{"'"}s not so boring Not anymore. + +`, + nodes: [ + { + id: '70:79', + name: 'clamp - hug', + type: 'FRAME', + visible: true, + parent: '60:26', + children: ['70:80'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 600, + height: 46, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '70:80', + name: "Getting into life 'Cause I found that it's not so boring Not anymore.", + type: 'TEXT', + visible: true, + parent: '70:79', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 500, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'ENDING', + maxLines: 1, + styledTextSegments: [ + { + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + start: 0, + end: 69, + fontSize: 16, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:26', + name: 'clamp', + type: 'SECTION', + children: ['70:79'], + }, + ], + variables: [], + }, + { + expected: ` + + Getting into life {"'"}Cause I found that it{"'"}s not so boring Not anymore. + +`, + nodes: [ + { + id: '70:92', + name: 'clamp - fill', + type: 'FRAME', + visible: true, + parent: '60:26', + children: ['70:93'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 600, + height: 46, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '70:93', + name: "Getting into life 'Cause I found that it's not so boring Not anymore.", + type: 'TEXT', + visible: true, + parent: '70:92', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 580, + height: 26, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'ENDING', + maxLines: 2, + styledTextSegments: [ + { + characters: + "Getting into life 'Cause I found that it's not so boring Not anymore.", + start: 0, + end: 69, + fontSize: 16, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:26', + name: 'clamp', + type: 'SECTION', + children: ['70:92'], + }, + ], + variables: [], + }, + // effect + { + expected: ``, + nodes: [ + { + id: '105:95', + name: 'boxShadow', + type: 'FRAME', + reactions: [], + parent: '105:13', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + isAsset: false, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 240, + cornerRadius: 20, + strokes: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.25, + }, + offset: { + x: 0, + y: 4, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + rotation: 0, + clipsContent: true, + visible: true, + width: 240, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + itemSpacing: 0, + counterAxisSpacing: 0, + strokeWeight: 3, + strokeTopWeight: 3, + strokeBottomWeight: 3, + strokeLeftWeight: 3, + strokeRightWeight: 3, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '105:13', + name: 'effect', + type: 'SECTION', + children: ['105:95'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:99', + name: 'inset', + type: 'FRAME', + visible: true, + parent: '105:13', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [ + { + type: 'INNER_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.25, + }, + offset: { + x: 0, + y: 4, + }, + spread: 0, + blendMode: 'NORMAL', + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeTopWeight: 3, + strokeBottomWeight: 3, + strokeLeftWeight: 3, + strokeRightWeight: 3, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '105:13', + name: 'effect', + type: 'SECTION', + children: ['105:99'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:103', + name: 'blur', + type: 'FRAME', + visible: true, + parent: '105:13', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [ + { + type: 'LAYER_BLUR', + visible: true, + radius: 4, + boundVariables: {}, + blurType: 'NORMAL', + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeTopWeight: 3, + strokeBottomWeight: 3, + strokeLeftWeight: 3, + strokeRightWeight: 3, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '105:13', + name: 'effect', + type: 'SECTION', + children: ['105:103'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '105:107', + name: 'backgroundBlur', + type: 'FRAME', + visible: true, + parent: '105:13', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0.20000000298023224, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [ + { + type: 'BACKGROUND_BLUR', + visible: true, + radius: 10, + boundVariables: {}, + blurType: 'NORMAL', + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 240, + height: 240, + rotation: 0, + cornerRadius: 20, + topLeftRadius: 20, + topRightRadius: 20, + bottomLeftRadius: 20, + bottomRightRadius: 20, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeTopWeight: 3, + strokeBottomWeight: 3, + strokeLeftWeight: 3, + strokeRightWeight: 3, + strokeAlign: 'CENTER', + dashPattern: [], + }, + { + id: '105:13', + name: 'effect', + type: 'SECTION', + children: ['105:107'], + }, + ], + variables: [], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 6904e6e..4842a1f 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -165,6 +165,14 @@ class NodeProxyTracker { 'maxHeight', 'targetAspectRatio', 'inferredAutoLayout', + // Stroke 속성 + 'strokeWeight', + 'strokeTopWeight', + 'strokeBottomWeight', + 'strokeLeftWeight', + 'strokeRightWeight', + 'strokeAlign', + 'dashPattern', // TEXT 노드 속성 'characters', 'fontName', @@ -599,6 +607,23 @@ export function assembleNodeTree( processBoundVariables(node.strokes) } + // strokeWeight가 없고 개별 stroke weights가 있으면 strokeWeight를 figma.mixed로 설정 + if ( + node.strokeWeight === undefined && + (node.strokeTopWeight !== undefined || + node.strokeBottomWeight !== undefined || + node.strokeLeftWeight !== undefined || + node.strokeRightWeight !== undefined) + ) { + const figmaGlobal = globalThis as unknown as { + figma?: { mixed?: unknown } + } + if (figmaGlobal.figma?.mixed) { + ;(node as unknown as Record).strokeWeight = + figmaGlobal.figma.mixed + } + } + // TEXT 노드에 getStyledTextSegments mock 메서드 추가 if (node.type === 'TEXT') { const textNode = node as NodeData & { styledTextSegments?: unknown[] } From 6272ee093fbaa7dde71fe86a0a85778244bdb8b5 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 28 Dec 2025 23:27:03 +0900 Subject: [PATCH 18/51] Add absolute --- src/codegen/__tests__/codegen.test.ts | 1191 +++++++++++++++++++++++++ src/codegen/utils/node-proxy.ts | 35 + 2 files changed, 1226 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index d0e6f9f..3d19c51 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -11270,6 +11270,1197 @@ describe('render real world component', () => { ], variables: [], }, + // Dimmend, absolute + { + expected: ` + + + + + 지금 시작할 수 있어요 + + + 내 삶을 더 잘 이해하는 첫 걸음 + + + + 자꾸 미뤄졌던 나에 대한 고민, 퍼즐핏에서 구체적인 답을 찾아보세요. + + + + + 회원가입 후, 유료로 진행됩니다 + + +`, + + nodes: [ + { + id: '107:26', + name: 'Container', + type: 'FRAME', + reactions: [], + parent: '109:84', + children: ['107:27', '107:28', '107:29', '107:32', '107:33'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 50, + paddingBottom: 50, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 1, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.9803921580314636, + g: 0.9607843160629272, + b: 1, + a: 1, + }, + position: 0, + boundVariables: { + color: + '[NodeId: VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227]', + }, + }, + { + color: { + r: 0.8784313797950745, + g: 0.9058823585510254, + b: 1, + a: 1, + }, + position: 1, + boundVariables: { + color: + '[NodeId: VariableID:417859516cc38076cb4af248fcd12020cd9ae4b2/27:25]', + }, + }, + ], + gradientTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + width: 360, + height: 311, + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 1, + paddingLeft: 20, + paddingRight: 20, + paddingTop: 50, + paddingBottom: 50, + itemSpacing: 20, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '107:27', + name: 'puzzle', + type: 'RECTANGLE', + visible: true, + parent: '107:26', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'e7958a16573afffb69800c3c66b7a5abdbd7bce3', + }, + ], + strokes: [], + effects: [ + { + type: 'LAYER_BLUR', + visible: true, + radius: 12, + boundVariables: {}, + blurType: 'PROGRESSIVE', + startRadius: 0, + startOffset: { + x: 0.5, + y: 0.3321799337863922, + }, + endOffset: { + x: 0.14488635957241058, + y: 0.8797577619552612, + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 300, + height: 332, + rotation: -8.82894874863189, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'ABSOLUTE', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + constraints: { + horizontal: 'MAX', + vertical: 'MIN', + }, + x: 168.009765625, + y: -76, + }, + { + id: '107:28', + name: 'dimmed', + type: 'RECTANGLE', + visible: true, + parent: '107:26', + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.929411768913269, + g: 0.9333333373069763, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.929411768913269, + g: 0.9333333373069763, + b: 1, + a: 0, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [-1.8426270287363877e-8, -1, 1], + [1, -3.776177948111581e-8, -1.5905754935374716e-8], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 360, + height: 311, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'ABSOLUTE', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + constraints: { + horizontal: 'MIN', + vertical: 'MIN', + }, + x: 0, + y: 0, + }, + { + id: '107:29', + name: 'Frame 105', + type: 'FRAME', + visible: true, + parent: '107:26', + children: ['107:30', '107:31'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 222, + height: 51, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 8, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 8, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '107:30', + name: '지금 시작할 수 있어요', + type: 'TEXT', + visible: true, + parent: '107:29', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.529411792755127, + g: 0.12156862765550613, + b: 0.9019607901573181, + }, + boundVariables: { + color: + '[NodeId: VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 97, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '지금 시작할 수 있어요', + fontName: { + family: 'Pretendard', + style: 'SemiBold', + }, + fontSize: 12, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '지금 시작할 수 있어요', + start: 0, + end: 12, + fontSize: 12, + fontName: { + family: 'Pretendard', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.529411792755127, + g: 0.12156862765550613, + b: 0.9019607901573181, + }, + boundVariables: { + color: + '[NodeId: VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47]', + }, + }, + ], + textStyleId: 'S:5ab6109ba578c1e241c1ec0231069c37ffab9ebe,27:18', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '107:31', + name: '내 삶을 더 잘 이해하는 첫 걸음', + type: 'TEXT', + visible: true, + parent: '107:29', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.06666667014360428, + g: 0.0941176488995552, + b: 0.15294118225574493, + }, + boundVariables: { + color: + '[NodeId: VariableID:099a1a094b0ad8776e9d00872465534eee0f1c2a/14:64]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 222, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '내 삶을 더 잘 이해하는 첫 걸음', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 20, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 120.00000476837158, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '내 삶을 더 잘 이해하는 첫 걸음', + start: 0, + end: 18, + fontSize: 20, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 120.00000476837158, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.06666667014360428, + g: 0.0941176488995552, + b: 0.15294118225574493, + }, + boundVariables: { + color: + '[NodeId: VariableID:099a1a094b0ad8776e9d00872465534eee0f1c2a/14:64]', + }, + }, + ], + textStyleId: 'S:22826d8fa5409d17932c82e7bda0b9c7306d99ca,34:65', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '107:32', + name: '자꾸 미뤄졌던 나에 대한 고민, 퍼즐핏에서 구체적인 답을 찾아보세요.', + type: 'TEXT', + visible: true, + parent: '107:26', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.21568627655506134, + g: 0.2549019753932953, + b: 0.3176470696926117, + }, + boundVariables: { + color: + '[NodeId: VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 320, + height: 44, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + '자꾸 미뤄졌던 나에 대한 고민, \n퍼즐핏에서 구체적인 답을 찾아보세요.', + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontSize: 14, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + '자꾸 미뤄졌던 나에 대한 고민, \n퍼즐핏에서 구체적인 답을 찾아보세요.', + start: 0, + end: 39, + fontSize: 14, + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.21568627655506134, + g: 0.2549019753932953, + b: 0.3176470696926117, + }, + boundVariables: { + color: + '[NodeId: VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65]', + }, + }, + ], + textStyleId: 'S:7d3ad4ba2c8903ffba997502193ac0827747a798,18:212', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '107:33', + name: 'Frame 106', + type: 'FRAME', + visible: true, + parent: '107:26', + children: ['107:34', '107:35'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 140, + height: 76, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '107:34', + name: 'GradientButton', + type: 'INSTANCE', + visible: true, + parent: '107:33', + children: ['I107:34;18:2206'], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5764706134796143, + g: 0.20000000298023224, + b: 0.9176470637321472, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.30980393290519714, + g: 0.27450981736183167, + b: 0.8980392217636108, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + strokes: [], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 2, + }, + spread: -2, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + { + type: 'DROP_SHADOW', + visible: true, + radius: 6, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 4, + }, + spread: -1, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 138, + height: 45, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: 'I107:34;18:2206', + name: 'Text', + type: 'TEXT', + visible: true, + parent: '107:34', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 78, + height: 21, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '검사 시작하기', + fontName: { + family: 'Pretendard', + style: 'SemiBold', + }, + fontSize: 15, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '검사 시작하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Pretendard', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:dc4ad9d035b632bb81cb467ad64f4bb9448ded87,18:206', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '107:35', + name: '회원가입 후, 유료로 진행됩니다', + type: 'TEXT', + visible: true, + parent: '107:33', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.29411765933036804, + g: 0.3333333432674408, + b: 0.38823530077934265, + }, + boundVariables: { + color: + '[NodeId: VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 0.5, + blendMode: 'PASS_THROUGH', + width: 140, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '회원가입 후, 유료로 진행됩니다', + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '회원가입 후, 유료로 진행됩니다', + start: 0, + end: 17, + fontSize: 12, + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 160.0000023841858, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.29411765933036804, + g: 0.3333333432674408, + b: 0.38823530077934265, + }, + boundVariables: { + color: + '[NodeId: VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70]', + }, + }, + ], + textStyleId: 'S:6104b34398c9cfd4de9323f16187c4cc20509420,27:15', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '109:84', + name: 'Dimmed와 absolute', + type: 'SECTION', + children: ['107:26'], + }, + ], + variables: [ + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:417859516cc38076cb4af248fcd12020cd9ae4b2/27:25', + name: 'secondaryBg', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:099a1a094b0ad8776e9d00872465534eee0f1c2a/14:64', + name: 'title', + }, + { + id: 'VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65', + name: 'text', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 4842a1f..2967e54 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -597,6 +597,26 @@ export function assembleNodeTree( bv.color = { id: colorId } } } + + // Gradient stops의 boundVariables 처리 + if (Array.isArray(p.gradientStops)) { + for (const stop of p.gradientStops) { + if (stop && typeof stop === 'object') { + const s = stop as Record + if (s.boundVariables && typeof s.boundVariables === 'object') { + const bv = s.boundVariables as Record + if (typeof bv.color === 'string') { + let colorId = bv.color + const match = colorId.match(/\[NodeId: ([^\]]+)\]/) + if (match) { + colorId = match[1] + } + bv.color = { id: colorId } + } + } + } + } + } } } } @@ -624,6 +644,21 @@ export function assembleNodeTree( } } + // INSTANCE 노드에 getMainComponentAsync mock 메서드 추가 + if (node.type === 'INSTANCE') { + ;(node as unknown as Record).getMainComponentAsync = + async () => { + // INSTANCE 노드는 mainComponent 속성을 참조하거나 null 반환 + return null + } + } + + // 모든 노드에 getMainComponentAsync 메서드 추가 (아직 없는 경우에만) + if (!(node as unknown as Record).getMainComponentAsync) { + ;(node as unknown as Record).getMainComponentAsync = + async () => null + } + // TEXT 노드에 getStyledTextSegments mock 메서드 추가 if (node.type === 'TEXT') { const textNode = node as NodeData & { styledTextSegments?: unknown[] } From a1282a96297a430e999ac88bb0a37175e99e7de4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 28 Dec 2025 23:31:31 +0900 Subject: [PATCH 19/51] Add svg test case --- src/codegen/__tests__/codegen.test.ts | 1053 +++++++++++++++++++++++++ 1 file changed, 1053 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 3d19c51..149fa65 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -12461,6 +12461,1059 @@ describe('render real world component', () => { }, ], }, + // svg + { + expected: ``, + nodes: [ + { + id: '171:1545', + name: 'recommend', + type: 'FRAME', + visible: true, + parent: '189:1752', + children: ['171:1546'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 280, + y: 280, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 3.4942855834960938, + paddingRight: 2.894681930541992, + paddingTop: 2.021327495574951, + paddingBottom: 2.962193012237549, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1546', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1545', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '189:1752', + name: 'SVG', + type: 'SECTION', + children: ['171:1545'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '171:1553', + name: 'recommend', + type: 'FRAME', + visible: true, + parent: '189:1752', + children: ['171:1559', '171:1554'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 19.611032485961914, + height: 34.032958984375, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1559', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1554', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '189:1752', + name: 'SVG', + type: 'SECTION', + children: ['171:1553'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '171:1561', + name: 'recommend', + type: 'FRAME', + visible: true, + parent: '189:1752', + children: ['171:1565', '171:1563'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 19.611032485961914, + height: 34.032958984375, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1565', + name: 'Group 2', + type: 'GROUP', + visible: true, + parent: '171:1561', + children: ['171:1562'], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '171:1562', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1565', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: -180, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1563', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1561', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '189:1752', + name: 'SVG', + type: 'SECTION', + children: ['171:1561'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '171:1566', + name: 'recommend', + type: 'FRAME', + visible: true, + parent: '189:1752', + children: ['171:1567', '171:1569'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 19.611032485961914, + height: 34.032958984375, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 3, + paddingRight: 3, + paddingTop: 2, + paddingBottom: 2, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1567', + name: 'Group 2', + type: 'GROUP', + visible: true, + parent: '171:1566', + children: ['171:1568'], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + }, + { + id: '171:1568', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1567', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4054486155509949, + g: 0.2079222947359085, + b: 0.2079222947359085, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: -180, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1569', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1566', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '189:1752', + name: 'SVG', + type: 'SECTION', + children: ['171:1566'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '171:1548', + name: 'recommend', + type: 'FRAME', + reactions: [], + parent: '189:1752', + children: ['188:1549', '171:1549'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 3, + layoutPositioning: 'AUTO', + }, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + width: 20, + height: 33.032958984375, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 3, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '188:1549', + name: 'Frame 1597884470', + type: 'FRAME', + visible: true, + parent: '171:1548', + children: ['171:1551'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1551', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1549', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '171:1549', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '171:1548', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.7676283121109009, + g: 0.1328587383031845, + b: 0.1328587383031845, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '189:1752', + name: 'SVG', + type: 'SECTION', + children: ['171:1548'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + // flex with maxW ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From fc8f7c6673193e5e83d49105be07eca88118a24f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 28 Dec 2025 23:38:56 +0900 Subject: [PATCH 20/51] Add svg test case --- src/codegen/__tests__/codegen.test.ts | 1727 +++++++++++++++++++++++++ 1 file changed, 1727 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 149fa65..6460fbd 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -13514,6 +13514,1733 @@ describe('render real world component', () => { ], }, // flex with maxW + { + expected: ` +
+ + Hello World! + + +
+
`, + nodes: [ + { + id: '60:2', + name: 'Container', + type: 'FRAME', + reactions: [], + parent: '35:5', + children: ['60:5'], + paddingLeft: 30, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + layoutMode: 'VERTICAL', + width: 1920, + height: 758, + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:5', + name: 'Frame 12', + type: 'FRAME', + visible: true, + parent: '60:2', + children: ['60:4', '60:7'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 598, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 50, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: 1280, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 50, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:4', + name: 'Hello World!', + type: 'TEXT', + visible: true, + parent: '60:5', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 228, + height: 48, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World!', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 40, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World!', + start: 0, + end: 12, + fontSize: 40, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:7', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '60:5', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 500, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '35:5', + name: 'Flex with maxW', + type: 'SECTION', + children: ['60:2'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ` +
+ + Hello World! + + +
+
`, + nodes: [ + { + id: '60:19', + name: 'imageRatio', + type: 'FRAME', + visible: true, + parent: '35:5', + children: ['60:20'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1920, + height: 978, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:20', + name: 'Frame 12', + type: 'FRAME', + visible: true, + parent: '60:19', + children: ['60:21', '60:22'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 818, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 50, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: 1280, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 50, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:21', + name: 'Hello World!', + type: 'TEXT', + visible: true, + parent: '60:20', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 228, + height: 48, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World!', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 40, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World!', + start: 0, + end: 12, + fontSize: 40, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:22', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '60:20', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 720, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 1280, + y: 720, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '35:5', + name: 'Flex with maxW', + type: 'SECTION', + children: ['60:19'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ` +
+ + Hello World! + + +
+
`, + nodes: [ + { + id: '60:27', + name: 'maxW maxH', + type: 'FRAME', + visible: true, + parent: '35:5', + children: ['60:28'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1920, + height: 760, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:28', + name: 'Frame 12', + type: 'FRAME', + visible: true, + parent: '60:27', + children: ['60:29', '60:30'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 600, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 50, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: 1280, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 50, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:29', + name: 'Hello World!', + type: 'TEXT', + visible: true, + parent: '60:28', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 228, + height: 48, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World!', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 40, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World!', + start: 0, + end: 12, + fontSize: 40, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:30', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '60:28', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 800, + height: 400, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: 800, + minHeight: null, + maxHeight: 400, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '35:5', + name: 'Flex with maxW', + type: 'SECTION', + children: ['60:27'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ` +
+ + Hello World! + + +
+
`, + nodes: [ + { + id: '60:31', + name: 'maxW', + type: 'FRAME', + visible: true, + parent: '35:5', + children: ['60:32'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1920, + height: 658, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:32', + name: 'Frame 12', + type: 'FRAME', + visible: true, + parent: '60:31', + children: ['60:33', '60:34'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 498, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 50, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: 1280, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 50, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:33', + name: 'Hello World!', + type: 'TEXT', + visible: true, + parent: '60:32', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 228, + height: 48, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World!', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 40, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World!', + start: 0, + end: 12, + fontSize: 40, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:34', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '60:32', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 800, + height: 400, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: 800, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '35:5', + name: 'Flex with maxW', + type: 'SECTION', + children: ['60:31'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + { + expected: ` +
+ + Hello World! + + +
+
`, + nodes: [ + { + id: '60:35', + name: 'maxH', + type: 'FRAME', + visible: true, + parent: '35:5', + children: ['60:36'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1920, + height: 760, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 80, + paddingBottom: 80, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:36', + name: 'Frame 12', + type: 'FRAME', + visible: true, + parent: '60:35', + children: ['60:37', '60:38'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 600, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 50, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: 1280, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 50, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '60:37', + name: 'Hello World!', + type: 'TEXT', + visible: true, + parent: '60:36', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 228, + height: 48, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World!', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 40, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World!', + start: 0, + end: 12, + fontSize: 40, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '60:38', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '60:36', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1024, + height: 400, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FILL', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: 1024, + minHeight: null, + maxHeight: 400, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '35:5', + name: 'Flex with maxW', + type: 'SECTION', + children: ['60:35'], + }, + ], + variables: [ + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + ], + }, + // Text with auto layout ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From c868fc0ed59134a2e391e164df87a83d4c5f50ba Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 00:18:13 +0900 Subject: [PATCH 21/51] Add text test case --- src/codegen/__tests__/codegen.test.ts | 2774 +++++++++++++++++++++++++ 1 file changed, 2774 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 6460fbd..a48c68a 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -15241,6 +15241,2780 @@ describe('render real world component', () => { ], }, // Text with auto layout + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:3', + name: 'Frame 2', + type: 'FRAME', + reactions: [], + parent: '36:24', + children: ['40:2', '40:4'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 400, + height: 60, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:2', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:3', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:4', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:3', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:3'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:57', + name: 'Frame 10', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:58', '40:59'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 429, + height: 35, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:58', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:57', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:59', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:57', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:57'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:9', + name: 'Frame 3', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:10', '40:11'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 200, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:10', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:9', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:11', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:9', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:9'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:78', + name: 'Frame 11', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:79', '40:80'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 600, + height: 35, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:79', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:78', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:80', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:78', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 485, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:78'], + }, + ], + variables: [], + }, + { + expected: `
+ + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
`, + nodes: [ + { + id: '40:18', + name: 'Frame 4', + type: 'FRAME', + reactions: [], + parent: '36:24', + children: ['40:19', '40:20'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 400, + height: 60, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:19', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:18', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:20', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:18', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:18'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:30', + name: 'Frame 6', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:31', '40:32'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 200, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:31', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:30', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:32', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:30', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:30'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:42', + name: 'Frame 8', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:43', '40:44'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 60, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:43', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:42', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 360, + height: 15, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:44', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:42', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 360, + height: 15, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:42'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:24', + name: 'Frame 5', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:25', '40:26'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 60, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'MAX', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MAX', + counterAxisAlignItems: 'MAX', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:25', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:24', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:26', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:24', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:24'], + }, + ], + variables: [], + }, + { + expected: ` + + Hello World + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +`, + nodes: [ + { + id: '40:36', + name: 'Frame 7', + type: 'FRAME', + visible: true, + parent: '36:24', + children: ['40:37', '40:38'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 400, + height: 200, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MAX', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MAX', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '40:37', + name: 'Hello World', + type: 'TEXT', + visible: true, + parent: '40:36', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 65, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: 'Hello World', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: 'Hello World', + start: 0, + end: 11, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '40:38', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'TEXT', + visible: true, + parent: '40:36', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 314, + height: 15, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontSize: 12, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + start: 0, + end: 56, + fontSize: 12, + fontName: { + family: 'Inter', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '36:24', + name: 'Text with auto layout', + type: 'SECTION', + children: ['40:36'], + }, + ], + variables: [], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 66509f1078fec9b87c64729ddb82a4f55a35ef5b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 00:19:46 +0900 Subject: [PATCH 22/51] Add object-fit test case --- src/codegen/__tests__/codegen.test.ts | 280 ++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index a48c68a..e0027e6 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -18015,6 +18015,286 @@ describe('render real world component', () => { ], variables: [], }, + { + expected: ` + +`, + nodes: [ + { + id: '69:9', + name: 'object-fit : cover (default)', + type: 'FRAME', + reactions: [], + parent: '69:18', + children: ['69:8'], + paddingLeft: 10, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + layoutMode: 'VERTICAL', + width: 1300, + height: 520, + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '69:8', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '69:9', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 0.5905511975288391, 0.20472441613674164], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 500, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '69:18', + name: 'object-fit', + type: 'SECTION', + children: ['69:9'], + }, + ], + variables: [], + }, + { + expected: ` + +`, + nodes: [ + { + id: '69:13', + name: 'object-fit : contain', + type: 'FRAME', + visible: true, + parent: '69:18', + children: ['69:14'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1300, + height: 520, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '69:14', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '69:13', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FIT', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: 'b39b8e5c5acff7a565c2d9f6d0e923160b0271f3', + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1280, + height: 500, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '69:18', + name: 'object-fit', + type: 'SECTION', + children: ['69:13'], + }, + ], + variables: [], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 540bdb7de93399ef46e794f1936f1177c52681b7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 00:21:15 +0900 Subject: [PATCH 23/51] Fix typography --- src/codegen/__tests__/codegen.test.ts | 994 ++++++++++++++++++++++++++ 1 file changed, 994 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index e0027e6..1153c59 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -18295,6 +18295,1000 @@ describe('render real world component', () => { ], variables: [], }, + { + expected: ` + + + 플레이 제목 + + + + 2025.03.03 18:00 + + + + + + 2 + + + + + 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 + +`, + nodes: [ + { + id: '109:51', + name: 'Card', + type: 'FRAME', + reactions: [], + parent: '187:1545', + children: ['109:52', '109:64'], + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 1, + itemSpacing: 16, + layoutPositioning: 'AUTO', + }, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8941176533699036, + g: 0.9137254953384399, + b: 0.9490196108818054, + }, + boundVariables: { + color: + '[NodeId: VariableID:f5613063210cb55c4f22591497c139340720b4f9/2:116]', + }, + }, + ], + dashPattern: [], + strokeWeight: 1, + strokeAlign: 'CENTER', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:48af241d35e3be1aa75c9b68e40b32ee5f7c2d40/2:200]', + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + layoutMode: 'VERTICAL', + width: 1060, + height: 104, + layoutAlign: 'INHERIT', + layoutGrow: 1, + paddingLeft: 20, + paddingRight: 20, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 16, + counterAxisSpacing: 0, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + }, + { + id: '109:52', + name: 'Frame 1597884450', + type: 'FRAME', + visible: true, + parent: '109:51', + children: ['109:53', '109:54', '109:55', '109:56', '109:57'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1020, + height: 24, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 8, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 8, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '109:53', + name: '플레이 제목', + type: 'TEXT', + visible: true, + parent: '109:52', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 69, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '플레이 제목', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '플레이 제목', + start: 0, + end: 6, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '109:54', + name: 'Ellipse 3618', + type: 'ELLIPSE', + visible: true, + parent: '109:52', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 0.20000000298023224, + blendMode: 'PASS_THROUGH', + width: 6, + height: 6, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 6, + y: 6, + }, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + arcData: { + startingAngle: 0, + endingAngle: 6.2831854820251465, + innerRadius: 0, + }, + }, + { + id: '109:55', + name: '2025.03.03 18:00', + type: 'TEXT', + visible: true, + parent: '109:52', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 112, + height: 23, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '2025.03.03 18:00', + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontSize: 15, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '2025.03.03 18:00', + start: 0, + end: 16, + fontSize: 15, + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + textStyleId: 'S:8de3e75e8aa9e74fc05518a1daedc751ba038ca3,2:423', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '109:56', + name: 'Ellipse 3619', + type: 'ELLIPSE', + visible: true, + parent: '109:52', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 0.20000000298023224, + blendMode: 'PASS_THROUGH', + width: 6, + height: 6, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 6, + y: 6, + }, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + arcData: { + startingAngle: 0, + endingAngle: 6.2831854820251465, + innerRadius: 0, + }, + }, + { + id: '109:57', + name: 'Frame 1597884461', + type: 'FRAME', + visible: true, + parent: '109:52', + children: ['109:58', '109:63'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 33, + height: 23, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 4, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 4, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '109:58', + name: 'recommend', + type: 'FRAME', + visible: true, + parent: '109:57', + children: ['109:62'], + fills: [ + { + type: 'SOLID', + visible: false, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 280, + y: 280, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '109:62', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '109:58', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.611032485961914, + height: 15.0164794921875, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + }, + { + id: '109:63', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '109:57', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 9, + height: 23, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '2', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 15, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: '2', + start: 0, + end: 1, + fontSize: 15, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4941176474094391, + g: 0.4941176474094391, + b: 0.4941176474094391, + }, + boundVariables: { + color: + '[NodeId: VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146]', + }, + }, + ], + textStyleId: 'S:f7a84b2aff07543a05b32108ab91010ac120d7c6,26:61', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '109:64', + name: '댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력', + type: 'TEXT', + visible: true, + parent: '109:51', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 1020, + height: 24, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: + '댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력', + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontSize: 16, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + styledTextSegments: [ + { + characters: + '댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력 댓글 내용 출력', + start: 0, + end: 44, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:0daf4d1717522daf9c82204ed1d91b4ead4935b4,35:31', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '187:1545', + name: 'Case 1 - MaskImage', + type: 'SECTION', + children: ['109:51'], + }, + ], + variables: [ + { + id: 'VariableID:f5613063210cb55c4f22591497c139340720b4f9/2:116', + name: 'border', + }, + { + id: 'VariableID:48af241d35e3be1aa75c9b68e40b32ee5f7c2d40/2:200', + name: 'innerBg', + }, + { + id: 'VariableID:041286802cda2ac64dfa81669076d76d0b63e802/2:146', + name: 'caption', + }, + { + id: 'VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137', + name: 'text', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 87a6eda4b21e810019f85a685310d1cfdb54d43f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 00:25:05 +0900 Subject: [PATCH 24/51] Fix typography --- src/codegen/__tests__/codegen.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 1153c59..8f2a0a6 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -19289,6 +19289,7 @@ describe('render real world component', () => { }, ], }, + // grid ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 0b71655d6297c61bdea5d7227126c5b5b820a4e2 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:04:01 +0900 Subject: [PATCH 25/51] Add testcase --- src/codegen/__tests__/codegen.test.ts | 369 +++++++++++++++++++++++++- src/codegen/props/grid-child.ts | 2 +- src/codegen/utils/check-asset-node.ts | 8 +- src/codegen/utils/node-proxy.ts | 4 + 4 files changed, 379 insertions(+), 4 deletions(-) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 8f2a0a6..8954e6f 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -1636,8 +1636,8 @@ describe('Codegen', () => { height: 100, } as unknown as FrameNode, expected: ` - - + + `, }, { @@ -19290,6 +19290,371 @@ describe('render real world component', () => { ], }, // grid + { + expected: ` + + + + +`, + + nodes: [ + { + id: '145:1912', + name: 'Grid', + type: 'FRAME', + visible: true, + parent: '145:1927', + children: ['145:1909', '145:1910', '145:1928', '145:1929'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 210, + height: 210, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'GRID', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'GRID', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 2, + gridRowGap: 10, + gridColumnGap: 10, + gridRowCount: 2, + }, + { + id: '145:1909', + name: 'Frame 1597884463', + type: 'FRAME', + visible: true, + parent: '145:1912', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 0, + gridRowAnchorIndex: 0, + gridColumnCount: 0, + }, + { + id: '145:1910', + name: 'Frame 1597884464', + type: 'FRAME', + visible: true, + parent: '145:1912', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 1, + gridRowAnchorIndex: 0, + gridColumnCount: 0, + }, + { + id: '145:1928', + name: 'Frame 1597884465', + type: 'FRAME', + visible: true, + parent: '145:1912', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 0, + gridRowAnchorIndex: 1, + gridColumnCount: 0, + }, + { + id: '145:1929', + name: 'Frame 1597884466', + type: 'FRAME', + visible: true, + parent: '145:1912', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 1, + gridRowAnchorIndex: 1, + gridColumnCount: 0, + }, + { + id: '145:1927', + name: 'Grid', + type: 'SECTION', + children: ['145:1912'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], diff --git a/src/codegen/props/grid-child.ts b/src/codegen/props/grid-child.ts index 31d823f..e98840d 100644 --- a/src/codegen/props/grid-child.ts +++ b/src/codegen/props/grid-child.ts @@ -13,7 +13,7 @@ export function getGridChildProps( const columnCount = parent.gridColumnCount const currentIdx = node.gridColumnAnchorIndex + node.gridRowAnchorIndex * columnCount - if (parent.children[currentIdx] !== node) + if (parent.children[currentIdx]?.id !== node.id) return { gridColumn: `${node.gridColumnAnchorIndex + 1} / span 1`, gridRow: `${node.gridRowAnchorIndex + 1} / span 1`, diff --git a/src/codegen/utils/check-asset-node.ts b/src/codegen/utils/check-asset-node.ts index fa2f90b..00e2941 100644 --- a/src/codegen/utils/check-asset-node.ts +++ b/src/codegen/utils/check-asset-node.ts @@ -25,7 +25,13 @@ export function checkAssetNode( node: SceneNode, nested = false, ): 'svg' | 'png' | null { - if (node.type === 'TEXT' || node.type === 'COMPONENT_SET') return null + if ( + node.type === 'TEXT' || + node.type === 'COMPONENT_SET' || + ('inferredAutoLayout' in node && + node.inferredAutoLayout?.layoutMode === 'GRID') + ) + return null // if node is an animation target (has keyframes), it should not be treated as an asset if (isAnimationTarget(node)) return null // vector must be svg diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 2967e54..fd05f1a 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -185,6 +185,10 @@ class NodeProxyTracker { 'textAlignVertical', 'textTruncation', 'maxLines', + // grid + 'gridColumnAnchorIndex', + 'gridRowAnchorIndex', + 'gridColumnCount', ] for (const prop of propsToTrack) { From c2ecfdd2897aa28c05555425f67827e768dd9a41 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:14:19 +0900 Subject: [PATCH 26/51] Add testcase --- src/codegen/__tests__/codegen.test.ts | 4888 +++++++++++++++++++++++++ 1 file changed, 4888 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 8954e6f..1fafe0c 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -19655,6 +19655,4894 @@ describe('render real world component', () => { }, ], }, + { + expected: ` + + +`, + nodes: [ + { + id: '169:1545', + name: 'Grid - 순서대로 정렬 X', + type: 'FRAME', + inferredAutoLayout: { + layoutMode: 'GRID', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + gridRowGap: 10, + gridColumnGap: 10, + gridColumnCount: 2, + gridRowCount: 4, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '145:1927', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 430, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + children: ['169:1553', '169:1566'], + rotation: 0, + clipsContent: false, + reactions: [], + visible: true, + layoutMode: 'GRID', + width: 210, + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 10, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '169:1553', + name: 'Frame 1597884468', + type: 'FRAME', + visible: true, + parent: '169:1545', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 0, + gridRowAnchorIndex: 3, + gridColumnCount: 0, + }, + { + id: '169:1566', + name: 'Frame 1597884469', + type: 'FRAME', + visible: true, + parent: '169:1545', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 1, + gridRowAnchorIndex: 2, + gridColumnCount: 0, + }, + { + id: '145:1927', + name: 'Grid', + type: 'SECTION', + children: ['169:1545'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, + { + expected: ` + +`, + nodes: [ + { + id: '169:1563', + name: 'Grid - child 가 1개밖에 없을 때', + type: 'FRAME', + visible: true, + parent: '145:1927', + children: ['169:1564'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 210, + height: 210, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'GRID', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'GRID', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 2, + gridRowGap: 10, + gridColumnGap: 10, + gridRowCount: 2, + }, + { + id: '169:1564', + name: 'Frame 1597884468', + type: 'FRAME', + visible: true, + parent: '169:1563', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 0, + gridRowAnchorIndex: 0, + gridColumnCount: 0, + }, + { + id: '145:1927', + name: 'Grid', + type: 'SECTION', + children: ['169:1563'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, + { + expected: ` + +`, + nodes: [ + { + id: '179:1554', + name: 'Grid - child 가 1개밖에 없을 때', + type: 'FRAME', + visible: true, + parent: '145:1927', + children: ['179:1555'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 210, + height: 210, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'GRID', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'GRID', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 2, + gridRowGap: 10, + gridColumnGap: 10, + gridRowCount: 2, + }, + { + id: '179:1555', + name: 'Frame 1597884468', + type: 'FRAME', + visible: true, + parent: '179:1554', + children: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.4117647111415863, + g: 0.49803921580314636, + b: 0.6235294342041016, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: 1, + gridRowAnchorIndex: 1, + gridColumnCount: 0, + }, + { + id: '145:1927', + name: 'Grid', + type: 'SECTION', + children: ['179:1554'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, + // mix blend + { + expected: ``, + nodes: [ + { + id: '245:1595', + name: 'image', + type: 'RECTANGLE', + reactions: [], + parent: '245:1608', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: '5cee49483aa40d5cbf1764b2d584efc365723714', + }, + ], + isAsset: true, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: { + x: 4096, + y: 4096, + }, + width: 413, + height: 413, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + opacity: 0.800000011920929, + blendMode: 'OVERLAY', + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '245:1608', + name: 'Mix Blend Mode', + type: 'SECTION', + children: ['245:1595'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '245:1600', + name: 'image', + type: 'RECTANGLE', + visible: true, + parent: '245:1608', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: '5cee49483aa40d5cbf1764b2d584efc365723714', + }, + ], + strokes: [], + effects: [], + opacity: 0.800000011920929, + blendMode: 'SCREEN', + width: 413, + height: 413, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 4096, + y: 4096, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '245:1608', + name: 'Mix Blend Mode', + type: 'SECTION', + children: ['245:1600'], + }, + ], + variables: [ + { + id: 'VariableID:a41981510611520a1c47fa7ab84eb8fc2ae29df4/38:20', + name: 'containerBackground', + }, + { + id: 'VariableID:7b0c74af99b6b0b15148212c8340b286b4f12630/38:82', + name: 'text', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/90:29', + name: 'primary', + }, + { + id: 'VariableID:1060bad3592be247585df7163f204873508facae/90:51', + name: 'primaryBold', + }, + ], + }, + { + expected: ``, + nodes: [ + { + id: '245:1607', + name: 'image', + type: 'RECTANGLE', + reactions: [], + parent: '245:1608', + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: '5cee49483aa40d5cbf1764b2d584efc365723714', + }, + ], + isAsset: true, + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + targetAspectRatio: { + x: 4096, + y: 4096, + }, + width: 413, + height: 413, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + opacity: 0.800000011920929, + blendMode: 'MULTIPLY', + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '245:1608', + name: 'Mix Blend Mode', + type: 'SECTION', + children: ['245:1607'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '245:1609', + name: 'Rectangle 39796', + type: 'RECTANGLE', + visible: true, + parent: '245:1608', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'SOFT_LIGHT', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '245:1608', + name: 'Mix Blend Mode', + type: 'SECTION', + children: ['245:1609'], + }, + ], + variables: [], + }, + { + expected: ` + 데브파이브 +`, + nodes: [ + { + id: '245:1614', + name: '데브파이브', + type: 'TEXT', + visible: true, + parent: '245:1608', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.09615384787321091, + b: 0.09615384787321091, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'DARKEN', + width: 308, + height: 90, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '데브파이브', + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontSize: 64, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '데브파이브', + start: 0, + end: 5, + fontSize: 64, + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.09615384787321091, + b: 0.09615384787321091, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '245:1608', + name: 'Mix Blend Mode', + type: 'SECTION', + children: ['245:1614'], + }, + ], + variables: [], + }, + // testcase dron + { + expected: ` + + 간편 로그인 연동 + + + + + 카카오 + + + 연결 전 + +
+ + 인증하기 + +
+
+ + + 구글 + + + cooolvita@gmail.com + +
+ + 인증하기 + +
+
+
+ + + + + 서비스 이용약관 + + + + + + 개인정보 처리방침 + + + + + + 회원 탈퇴 + + + + +
`, + nodes: [ + { + id: '113:9', + name: 'Section1', + type: 'FRAME', + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 30, + paddingBottom: 30, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MAX', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 20, + layoutPositioning: 'AUTO', + }, + reactions: [], + parent: '189:1785', + children: ['113:10', '113:11', '113:22', '113:23'], + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MAX', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8941176533699036, + g: 0.9137254953384399, + b: 0.9490196108818054, + }, + boundVariables: { + color: + '[NodeId: VariableID:f5613063210cb55c4f22591497c139340720b4f9/2:116]', + }, + }, + ], + dashPattern: [], + strokeWeight: 1, + strokeAlign: 'INSIDE', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9843137264251709, + g: 0.9882352948188782, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:c94c4b118e67ccdd520018116c235719f1c770a6/7:138]', + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + layoutMode: 'VERTICAL', + width: 1060, + height: 391, + layoutAlign: 'INHERIT', + layoutGrow: 0, + paddingLeft: 40, + paddingRight: 40, + paddingTop: 30, + paddingBottom: 30, + itemSpacing: 20, + counterAxisSpacing: 0, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:10', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:9', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 30, + rotation: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '간편 로그인 연동', + fontName: { + family: 'SUIT', + style: 'Bold', + }, + fontSize: 20, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '간편 로그인 연동', + start: 0, + end: 9, + fontSize: 20, + fontName: { + family: 'SUIT', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:1a8fd81b1ba260740f7ddf31569a7ab10c0ec384,23:15', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:11', + name: 'Frame 1597884446', + type: 'FRAME', + visible: true, + parent: '113:9', + children: ['113:12', '113:17'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 96, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:12', + name: 'Frame 14697', + type: 'FRAME', + visible: true, + parent: '113:11', + children: ['113:13', '113:14', '113:15'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 48, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:13', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:12', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 801, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '카카오', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '카카오', + start: 0, + end: 3, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:14', + name: '723,457 hotels', + type: 'TEXT', + visible: true, + parent: '113:12', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5046297907829285, + g: 0.5046297907829285, + b: 0.5555557012557983, + }, + boundVariables: { + color: + '[NodeId: VariableID:b9921dbe36f26ff05978e014dac901b29f2a12f4/2:362]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 42, + height: 23, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '연결 전', + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontSize: 15, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '연결 전', + start: 0, + end: 4, + fontSize: 15, + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5046297907829285, + g: 0.5046297907829285, + b: 0.5555557012557983, + }, + boundVariables: { + color: + '[NodeId: VariableID:b9921dbe36f26ff05978e014dac901b29f2a12f4/2:362]', + }, + }, + ], + textStyleId: 'S:8de3e75e8aa9e74fc05518a1daedc751ba038ca3,2:423', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:15', + name: 'Frame 48098137', + type: 'FRAME', + visible: true, + parent: '113:12', + children: ['113:16'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24313725531101227, + g: 0.545098066329956, + b: 0.886274516582489, + }, + boundVariables: { + color: + '[NodeId: VariableID:62aa73c4d05c5df2860e80c700538556e877e723/2:69]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24313725531101227, + g: 0.545098066329956, + b: 0.886274516582489, + }, + boundVariables: { + color: + '[NodeId: VariableID:62aa73c4d05c5df2860e80c700538556e877e723/2:69]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 73, + height: 24, + rotation: 0, + cornerRadius: 100, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 4, + paddingBottom: 4, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 4, + paddingBottom: 4, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:16', + name: '인증하기', + type: 'TEXT', + visible: true, + parent: '113:15', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 49, + height: 16, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '인증하기', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'PIXELS', + value: 16, + }, + letterSpacing: { + unit: 'PIXELS', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '인증하기', + start: 0, + end: 4, + fontSize: 14, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PIXELS', + value: 16, + }, + letterSpacing: { + unit: 'PIXELS', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:a3848da5fdda46625c1895179c2bcba88a32539a,24:34', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:17', + name: 'Frame 14696', + type: 'FRAME', + visible: true, + parent: '113:11', + children: ['113:18', '113:19', '113:20'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 48, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:18', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:17', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 711, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '구글', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '구글', + start: 0, + end: 2, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:19', + name: '723,457 hotels', + type: 'TEXT', + visible: true, + parent: '113:17', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5046297907829285, + g: 0.5046297907829285, + b: 0.5555557012557983, + }, + boundVariables: { + color: + '[NodeId: VariableID:b9921dbe36f26ff05978e014dac901b29f2a12f4/2:362]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 132, + height: 23, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: 'cooolvita@gmail.com', + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontSize: 15, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: 'cooolvita@gmail.com', + start: 0, + end: 19, + fontSize: 15, + fontName: { + family: 'SUIT', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -0.5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5046297907829285, + g: 0.5046297907829285, + b: 0.5555557012557983, + }, + boundVariables: { + color: + '[NodeId: VariableID:b9921dbe36f26ff05978e014dac901b29f2a12f4/2:362]', + }, + }, + ], + textStyleId: 'S:8de3e75e8aa9e74fc05518a1daedc751ba038ca3,2:423', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:20', + name: 'Frame 48098137', + type: 'FRAME', + visible: true, + parent: '113:17', + children: ['113:21'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24313725531101227, + g: 0.545098066329956, + b: 0.886274516582489, + }, + boundVariables: { + color: + '[NodeId: VariableID:62aa73c4d05c5df2860e80c700538556e877e723/2:69]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24313725531101227, + g: 0.545098066329956, + b: 0.886274516582489, + }, + boundVariables: { + color: + '[NodeId: VariableID:62aa73c4d05c5df2860e80c700538556e877e723/2:69]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 73, + height: 24, + rotation: 0, + cornerRadius: 100, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 4, + paddingBottom: 4, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 4, + paddingBottom: 4, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:21', + name: '인증하기', + type: 'TEXT', + visible: true, + parent: '113:20', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 49, + height: 16, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '인증하기', + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'PIXELS', + value: 16, + }, + letterSpacing: { + unit: 'PIXELS', + value: 0, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '인증하기', + start: 0, + end: 4, + fontSize: 14, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PIXELS', + value: 16, + }, + letterSpacing: { + unit: 'PIXELS', + value: 0, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:a3848da5fdda46625c1895179c2bcba88a32539a,24:34', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:22', + name: 'Rectangle 39794', + type: 'RECTANGLE', + visible: true, + parent: '113:9', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8941176533699036, + g: 0.9137254953384399, + b: 0.9490196108818054, + }, + boundVariables: { + color: + '[NodeId: VariableID:f5613063210cb55c4f22591497c139340720b4f9/2:116]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 1, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '113:23', + name: 'Frame 1597884445', + type: 'FRAME', + visible: true, + parent: '113:9', + children: ['113:24', '113:28', '113:32'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 144, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'VERTICAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:24', + name: 'Frame 14701', + type: 'FRAME', + visible: true, + parent: '113:23', + children: ['113:25', '113:26'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 48, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:25', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:24', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 904, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '서비스 이용약관', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '서비스 이용약관', + start: 0, + end: 8, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:26', + name: 'ic:round-arrow-left', + type: 'FRAME', + visible: true, + parent: '113:24', + children: ['113:27'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 24, + height: 24, + rotation: -180, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:27', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '113:26', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6196078658103943, + g: 0.6196078658103943, + b: 0.6196078658103943, + }, + boundVariables: { + color: + '[NodeId: VariableID:6e4f1696da68f73d5c140882dee1f94e56df5222/7:272]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 4.59244441986084, + height: 7.1804890632629395, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '113:28', + name: 'Frame 14699', + type: 'FRAME', + visible: true, + parent: '113:23', + children: ['113:29', '113:30'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 48, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:29', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:28', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 904, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '개인정보 처리방침', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '개인정보 처리방침', + start: 0, + end: 9, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:30', + name: 'ic:round-arrow-left', + type: 'FRAME', + visible: true, + parent: '113:28', + children: ['113:31'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 24, + height: 24, + rotation: -180, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:31', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '113:30', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6196078658103943, + g: 0.6196078658103943, + b: 0.6196078658103943, + }, + boundVariables: { + color: + '[NodeId: VariableID:6e4f1696da68f73d5c140882dee1f94e56df5222/7:272]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 4.59244441986084, + height: 7.1804890632629395, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '113:32', + name: 'Frame 14700', + type: 'FRAME', + visible: true, + parent: '113:23', + children: ['113:33', '113:34'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 980, + height: 48, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'STRETCH', + layoutGrow: 0, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 12, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 12, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:33', + name: 'Hotels', + type: 'TEXT', + visible: true, + parent: '113:32', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 904, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FILL', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 0, + strokeAlign: 'CENTER', + dashPattern: [], + characters: '회원 탈퇴', + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontSize: 16, + fontWeight: 600, + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + textAutoResize: 'HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '회원 탈퇴', + start: 0, + end: 5, + fontSize: 16, + fontName: { + family: 'SUIT', + style: 'SemiBold', + }, + fontWeight: 600, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 150, + }, + letterSpacing: { + unit: 'PIXELS', + value: -1, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.22745098173618317, + g: 0.20000000298023224, + b: 0.2078431397676468, + }, + boundVariables: { + color: + '[NodeId: VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137]', + }, + }, + ], + textStyleId: 'S:ff39719a4f4432493ec910934cf96b8acdefc1ab,7:190', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '113:34', + name: 'ic:round-arrow-left', + type: 'FRAME', + visible: true, + parent: '113:32', + children: ['113:35'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 24, + height: 24, + rotation: -180, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '113:35', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '113:34', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6196078658103943, + g: 0.6196078658103943, + b: 0.6196078658103943, + }, + boundVariables: { + color: + '[NodeId: VariableID:6e4f1696da68f73d5c140882dee1f94e56df5222/7:272]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 4.59244441986084, + height: 7.1804890632629395, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1785', + name: 'Test Case : 드론아레나', + type: 'SECTION', + children: ['113:9'], + }, + ], + variables: [ + { + id: 'VariableID:f5613063210cb55c4f22591497c139340720b4f9/2:116', + name: 'border', + }, + { + id: 'VariableID:c94c4b118e67ccdd520018116c235719f1c770a6/7:138', + name: 'primaryBg', + }, + { + id: 'VariableID:57475e52516076fee3a8936d875fd8baaa61a342/2:137', + name: 'text', + }, + { + id: 'VariableID:b9921dbe36f26ff05978e014dac901b29f2a12f4/2:362', + name: 'gray500', + }, + { + id: 'VariableID:62aa73c4d05c5df2860e80c700538556e877e723/2:69', + name: 'primary', + }, + { + id: 'VariableID:6e4f1696da68f73d5c140882dee1f94e56df5222/7:272', + name: 'gray300', + }, + ], + }, + // many shape + { + expected: ``, + nodes: [ + { + id: '188:1567', + name: 'Star7', + type: 'STAR', + reactions: [], + parent: '188:1572', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + width: 100, + height: 100, + cornerRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: true, + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['188:1567'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '188:1568', + name: 'Polygon1', + type: 'VECTOR', + reactions: [], + parent: '188:1572', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + width: 100, + height: 100, + cornerRadius: 0, + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: true, + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['188:1568'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '189:1590', + name: 'Polygon1', + type: 'POLYGON', + visible: true, + parent: '188:1572', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8509804010391235, + g: 0.8509804010391235, + b: 0.8509804010391235, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 100, + height: 100, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['189:1590'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '188:1569', + name: 'Line 1', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 140, + height: 0, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 4, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['188:1569'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '216:1548', + name: 'Line 8', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 0.5, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 140, + height: 0, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['216:1548'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '189:1582', + name: 'Line 2', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 140, + height: 0, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['189:1582'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '189:1586', + name: 'Line 3', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 140, + height: 0, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 4, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['189:1586'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '199:1545', + name: 'Line4', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 150, + height: 0, + rotation: 15.000000120055855, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['199:1545'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '199:1554', + name: 'Line 7', + type: 'LINE', + visible: true, + parent: '188:1572', + fills: [], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 131, + height: 0, + rotation: 90, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['199:1554'], + }, + ], + variables: [], + }, + { + expected: ``, + nodes: [ + { + id: '188:1571', + name: 'Vector4', + type: 'VECTOR', + reactions: [], + parent: '188:1572', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + width: 280, + height: 60, + cornerRadius: 0, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + dashPattern: [], + strokeWeight: 1, + strokeAlign: 'CENTER', + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1572', + name: '다양한 도형', + type: 'SECTION', + children: ['188:1571'], + }, + ], + variables: [], + }, + // svg detail + { + expected: ``, + nodes: [ + { + id: '188:1552', + name: 'DevupUI', + type: 'FRAME', + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'MIN', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 8, + layoutPositioning: 'AUTO', + }, + reactions: [], + parent: '189:1599', + children: ['188:1553', '188:1557'], + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 28, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 0, + strokes: [], + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: false, + visible: true, + width: 128, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 8, + counterAxisSpacing: 0, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '188:1553', + name: 'Group 1', + type: 'GROUP', + visible: true, + parent: '188:1552', + children: ['188:1554', '188:1555', '188:1556'], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1554', + name: 'Star 5', + type: 'STAR', + visible: true, + parent: '188:1553', + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 1, + g: 0.8097218871116638, + b: 0.9968286752700806, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.6626253724098206, + g: 0.7285662889480591, + b: 0.9263890385627747, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1555', + name: 'Star 6', + type: 'STAR', + visible: true, + parent: '188:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0.4000000059604645, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1556', + name: 'Vector 2', + type: 'VECTOR', + visible: true, + parent: '188:1553', + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.9651618599891663, + g: 0.940277636051178, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 16.983606338500977, + height: 16.180328369140625, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1557', + name: 'Frame 3', + type: 'FRAME', + visible: true, + parent: '188:1552', + children: ['188:1558'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 92, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '188:1558', + name: 'Devup UI', + type: 'GROUP', + visible: true, + parent: '188:1557', + children: [ + '188:1559', + '188:1560', + '188:1561', + '188:1562', + '188:1563', + '188:1564', + '188:1565', + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 92, + height: 20, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1559', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 2.97971248626709, + height: 15.238091468811035, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1560', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.797364234924316, + height: 15.471328735351562, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1561', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12.350691795349121, + height: 16.30708885192871, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1562', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 10.839245796203613, + height: 11.545185089111328, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1563', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 11.983627319335938, + height: 11.350822448730469, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1564', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12.350696563720703, + height: 11.739547729492188, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '188:1565', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '188:1558', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.538261413574219, + height: 15.238091468811035, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['188:1552'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 30d4c7a3fd9930457c66acd4dfa418879d920d8b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:22:03 +0900 Subject: [PATCH 27/51] Add testcase --- src/codegen/__tests__/codegen.test.ts | 3482 +++++++++++++++++++++++++ 1 file changed, 3482 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 1fafe0c..20b5b2b 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -24543,6 +24543,3488 @@ describe('render real world component', () => { }, ], }, + { + expected: ``, + nodes: [ + { + id: '189:1760', + name: 'DevupUI', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1761', '189:1765'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 128, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 8, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 8, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1761', + name: 'Icon', + type: 'GROUP', + visible: true, + parent: '189:1760', + children: ['189:1762', '189:1763', '189:1764'], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 28, + y: 28, + }, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1762', + name: 'Star 5', + type: 'STAR', + visible: true, + parent: '189:1761', + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 1, + g: 0.8097218871116638, + b: 0.9968286752700806, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.6626253724098206, + g: 0.7285662889480591, + b: 0.9263890385627747, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1763', + name: 'Star 6', + type: 'STAR', + visible: true, + parent: '189:1761', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0.4000000059604645, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1764', + name: 'Vector 2', + type: 'VECTOR', + visible: true, + parent: '189:1761', + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.9651618599891663, + g: 0.940277636051178, + b: 1, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 1, + g: 1, + b: 1, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [6.123234262925839e-17, 1, 0], + [-1, 6.123234262925839e-17, 1], + ], + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 16.983606338500977, + height: 16.180328369140625, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1765', + name: 'Logo', + type: 'FRAME', + visible: true, + parent: '189:1760', + children: ['189:1766'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 92, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 6.6666669845581055, + paddingBottom: 1.3333330154418945, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1766', + name: 'Devup UI', + type: 'GROUP', + visible: true, + parent: '189:1765', + children: [ + '189:1767', + '189:1768', + '189:1769', + '189:1770', + '189:1771', + '189:1772', + '189:1773', + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 92, + height: 20, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'NONE', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'STRETCH', + layoutGrow: 1, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1767', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 2.97971248626709, + height: 15.238091468811035, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1768', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.797364234924316, + height: 15.471328735351562, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1769', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12.350691795349121, + height: 16.30708885192871, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1770', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 10.839245796203613, + height: 11.545185089111328, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1771', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 11.983627319335938, + height: 11.350822448730469, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1772', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 12.350696563720703, + height: 11.739547729492188, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1773', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1766', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.18431372940540314, + g: 0.18431372940540314, + b: 0.18431372940540314, + }, + boundVariables: { + color: + '[NodeId: VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 13.538261413574219, + height: 15.238091468811035, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1760'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1695', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1726'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:331c673285290a0918108f25061f5c757824db3b/11:20]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8980392217636108, + g: 0.9058823585510254, + b: 0.9215686321258545, + }, + boundVariables: { + color: + '[NodeId: VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 40, + height: 40, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 8, + paddingRight: 8, + paddingTop: 8, + paddingBottom: 8, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 8, + paddingRight: 8, + paddingTop: 8, + paddingBottom: 8, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1726', + name: 'back', + type: 'FRAME', + visible: true, + parent: '189:1695', + children: ['189:1727'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 24, + height: 24, + rotation: -180, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 16, + y: 16, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 8.99951171875, + paddingRight: 6.999662399291992, + paddingTop: 4.99951171875, + paddingBottom: 4.999663352966309, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1727', + name: 'Vector (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1726', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.29411765933036804, + g: 0.3333333432674408, + b: 0.38823530077934265, + }, + boundVariables: { + color: + '[NodeId: VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 8.000825881958008, + height: 14.000824928283691, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1.3333333730697632, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1695'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1690', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1731'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9803921580314636, + g: 0.9607843160629272, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 40, + height: 40, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 48, + y: 48, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1731', + name: 'plus', + type: 'FRAME', + visible: true, + parent: '189:1690', + children: ['189:1732'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 16, + y: 16, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 2.2916667461395264, + paddingRight: 2.291666269302368, + paddingTop: 2.2916667461395264, + paddingBottom: 2.291666269302368, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1732', + name: 'Vector 1 (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1731', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.529411792755127, + g: 0.12156862765550613, + b: 0.9019607901573181, + }, + boundVariables: { + color: + '[NodeId: VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 15.416666984558105, + height: 15.416666984558105, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2.5, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1690'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1744', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1745'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9803921580314636, + g: 0.9607843160629272, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 40, + height: 40, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 48, + y: 48, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1745', + name: 'plus', + type: 'FRAME', + visible: true, + parent: '189:1744', + children: ['189:1746', '189:1747'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 16, + y: 16, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1746', + name: 'Vector 1 (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1745', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.841636061668396, + g: 0.6682692170143127, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 15.416666984558105, + height: 15.416666984558105, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2.5, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1747', + name: 'Vector 1 (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1745', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.529411792755127, + g: 0.12156862765550613, + b: 0.9019607901573181, + }, + boundVariables: { + color: + '[NodeId: VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 15.416666984558105, + height: 15.416666984558105, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2.5, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1744'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1719', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1720'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 48, + height: 48, + rotation: 0, + cornerRadius: 100, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1720', + name: 'cog', + type: 'FRAME', + visible: true, + parent: '189:1719', + children: ['189:1721'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 24, + y: 24, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 2.6491129398345947, + paddingRight: 2.654266119003296, + paddingTop: 2.3333332538604736, + paddingBottom: 2.3333327770233154, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1721', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1720', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 22.69662094116211, + height: 23.33333396911621, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1719'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1744', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1745'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9803921580314636, + g: 0.9607843160629272, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 40, + height: 40, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 48, + y: 48, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1745', + name: 'plus', + type: 'FRAME', + visible: true, + parent: '189:1744', + children: ['189:1746', '189:1747'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 20, + height: 20, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 16, + y: 16, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1746', + name: 'Vector 1 (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1745', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.841636061668396, + g: 0.6682692170143127, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 15.416666984558105, + height: 15.416666984558105, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2.5, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1747', + name: 'Vector 1 (Stroke)', + type: 'VECTOR', + visible: true, + parent: '189:1745', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.529411792755127, + g: 0.12156862765550613, + b: 0.9019607901573181, + }, + boundVariables: { + color: + '[NodeId: VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 15.416666984558105, + height: 15.416666984558105, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2.5, + strokeAlign: 'CENTER', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1744'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1719', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1720'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 48, + height: 48, + rotation: 0, + cornerRadius: 100, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1720', + name: 'cog', + type: 'FRAME', + visible: true, + parent: '189:1719', + children: ['189:1721'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 24, + y: 24, + }, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 2.6491129398345947, + paddingRight: 2.654266119003296, + paddingTop: 2.3333332538604736, + paddingBottom: 2.3333327770233154, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'FIXED', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'MIN', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1721', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1720', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 22.69662094116211, + height: 23.33333396911621, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1719'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + { + expected: `
+ +
`, + nodes: [ + { + id: '189:1736', + name: 'Button', + type: 'FRAME', + visible: true, + parent: '189:1599', + children: ['189:1737'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 48, + height: 48, + rotation: 0, + cornerRadius: 100, + topLeftRadius: 100, + topRightRadius: 100, + bottomLeftRadius: 100, + bottomRightRadius: 100, + layoutMode: 'VERTICAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1737', + name: 'cog', + type: 'FRAME', + visible: true, + parent: '189:1736', + children: ['189:1738', '189:1739'], + fills: [], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 28, + height: 28, + rotation: 0, + cornerRadius: 0, + topLeftRadius: 0, + topRightRadius: 0, + bottomLeftRadius: 0, + bottomRightRadius: 0, + layoutMode: 'NONE', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: true, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + targetAspectRatio: { + x: 24, + y: 24, + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + }, + { + id: '189:1738', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1737', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 0.1586538404226303, + b: 0.1586538404226303, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 22.69662094116211, + height: 23.33333396911621, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1739', + name: 'Vector', + type: 'VECTOR', + visible: true, + parent: '189:1737', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 22.69662094116211, + height: 23.33333396911621, + rotation: 0, + cornerRadius: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + }, + { + id: '189:1599', + name: 'SVG - detail', + type: 'SECTION', + children: ['189:1736'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + ], + }, + // Decorative Text + { + expected: ` + 16.8s +`, + nodes: [ + { + id: '241:1549', + name: '16.8s', + type: 'TEXT', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '244:1553', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + textAutoResize: 'WIDTH_AND_HEIGHT', + strokes: [], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.4188108444213867, + g: 0.6934240460395813, + b: 0.950320303440094, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.5100101232528687, + g: 0.20721584558486938, + b: 0.7932692170143127, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [-1, 6.123234262925839e-17, 1], + [-9.68716443142322e-17, -1.5820338726043701, 1.291016936302185], + ], + }, + ], + width: 87, + height: 47, + opacity: 1, + blendMode: 'PASS_THROUGH', + characters: '16.8s', + isAsset: false, + textTruncation: 'DISABLED', + effects: [], + rotation: 0, + reactions: [], + visible: true, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 36, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 129.99999523162842, + }, + letterSpacing: { + unit: 'PERCENT', + value: -3, + }, + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '16.8s', + start: 0, + end: 5, + fontSize: 36, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 129.99999523162842, + }, + letterSpacing: { + unit: 'PERCENT', + value: -3, + }, + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.4188108444213867, + g: 0.6934240460395813, + b: 0.950320303440094, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.5100101232528687, + g: 0.20721584558486938, + b: 0.7932692170143127, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [-1, 6.123234262925839e-17, 1], + [ + -9.68716443142322e-17, -1.5820338726043701, + 1.291016936302185, + ], + ], + }, + ], + textStyleId: 'S:4dd924de606291d62751e055b2339398e6c20129,19:44', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '244:1553', + name: 'Decorative Text', + type: 'SECTION', + children: ['241:1549'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0', + name: 'primary', + }, + ], + }, + { + expected: ` + 홈페이지와 앱 제작 +`, + nodes: [ + { + id: '244:1548', + name: '홈페이지와 앱 제작', + type: 'TEXT', + visible: true, + parent: '244:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.29826346039772034, + g: 0.02083294279873371, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 12, + boundVariables: {}, + color: { + r: 0.48627451062202454, + g: 0.43921568989753723, + b: 1, + a: 1, + }, + offset: { + x: 0, + y: 0, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 319, + height: 46, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 1, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 3, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '홈페이지와 앱 제작', + fontName: { + family: 'Jalnan 2 TTF', + style: 'Regular', + }, + fontSize: 38, + fontWeight: 400, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -2, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '홈페이지와 앱 제작', + start: 0, + end: 10, + fontSize: 38, + fontName: { + family: 'Jalnan 2 TTF', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -2, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.29826346039772034, + g: 0.02083294279873371, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '244:1553', + name: 'Decorative Text', + type: 'SECTION', + children: ['244:1548'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0', + name: 'primary', + }, + ], + }, + { + expected: ` + + 온글 + + 과 함께라면 +`, + nodes: [ + { + id: '244:1564', + name: '온글과 함께라면', + type: 'TEXT', + visible: true, + parent: '244:1553', + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 242, + height: 59, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '온글과 함께라면', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '온글', + start: 0, + end: 2, + fontSize: 42, + fontName: { + family: 'Pretendard', + style: 'ExtraBold', + }, + fontWeight: 800, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.2980392277240753, + g: 0.24313725531101227, + b: 0.9490196108818054, + }, + boundVariables: { + color: + '[NodeId: VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0]', + }, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + { + characters: '과 함께라면', + start: 2, + end: 8, + fontSize: 40, + fontName: { + family: 'Pretendard', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0, + g: 0, + b: 0, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '244:1553', + name: 'Decorative Text', + type: 'SECTION', + children: ['244:1564'], + }, + ], + variables: [ + { + id: 'VariableID:228af354eb8741f69bcd91fe730fec0505e9eac9/19:40', + name: 'text', + }, + { + id: 'VariableID:5ed5fe4e2c110aae522cfe81f189c59552683358/18:227', + name: 'primaryBgLight', + }, + { + id: 'VariableID:51a40441a4e76d70d58452cb534b842a89c22c63/14:47', + name: 'primary', + }, + { + id: 'VariableID:331c673285290a0918108f25061f5c757824db3b/11:20', + name: 'containerBackground', + }, + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:93d8aadc9ec1c35e7c24bdf723b9b05a01b75a2d/14:70', + name: 'textLight', + }, + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0', + name: 'primary', + }, + ], + }, + { + expected: ` + 데브파이브 +`, + nodes: [ + { + id: '244:1573', + name: '데브파이브', + type: 'TEXT', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '244:1553', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + textAutoResize: 'WIDTH_AND_HEIGHT', + strokes: [], + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: '501cbd2c326417bbe006176f93aa0d7429d935b2', + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + characters: '데브파이브', + isAsset: true, + textTruncation: 'DISABLED', + effects: [], + rotation: 0, + reactions: [], + visible: true, + width: 308, + height: 90, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontSize: 64, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '데브파이브', + start: 0, + end: 5, + fontSize: 64, + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + fills: [ + { + type: 'IMAGE', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + scaleMode: 'FILL', + imageTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + scalingFactor: 0.5, + rotation: 0, + filters: { + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 0, + tint: 0, + highlights: 0, + shadows: 0, + }, + imageHash: '501cbd2c326417bbe006176f93aa0d7429d935b2', + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '244:1553', + name: 'Decorative Text', + type: 'SECTION', + children: ['244:1573'], + }, + ], + variables: [ + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0', + name: 'primary', + }, + ], + }, + { + expected: ` + 데브파이브 +`, + nodes: [ + { + id: '244:1578', + name: '데브파이브', + type: 'TEXT', + visible: true, + parent: '244:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 12, + boundVariables: {}, + color: { + r: 0.48627451062202454, + g: 0.43921568989753723, + b: 1, + a: 1, + }, + offset: { + x: 0, + y: 0, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + { + type: 'DROP_SHADOW', + visible: true, + radius: 24, + boundVariables: {}, + color: { + r: 1, + g: 0.7868587970733643, + b: 0.9964476227760315, + a: 0.4000000059604645, + }, + offset: { + x: 0, + y: 10, + }, + spread: 0, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 308, + height: 90, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 2, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '데브파이브', + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontSize: 64, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '데브파이브', + start: 0, + end: 5, + fontSize: 64, + fontName: { + family: 'Jalnan Gothic TTF', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -5, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 0, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: '', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '244:1553', + name: 'Decorative Text', + type: 'SECTION', + children: ['244:1578'], + }, + ], + variables: [ + { + id: 'VariableID:4be0713ea9f483833095fc5a0baa288fccc81e47/173:0', + name: 'primary', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 782eed8727675d6dcc1c957fd92ae03d6272e554 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:37:13 +0900 Subject: [PATCH 28/51] Add testcase --- src/codegen/props/selector.ts | 2 +- src/codegen/render/index.ts | 12 +++++++++--- src/codegen/responsive/ResponsiveCodegen.ts | 15 +++++++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index f38da9f..98cb2f6 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -58,7 +58,7 @@ export async function getSelectorProps( const result = Object.entries(node.componentPropertyDefinitions).reduce( (acc, [name, definition]) => { - if (name !== 'effect') { + if (name !== 'effect' && name !== 'viewport') { const sanitizedName = sanitizePropertyName(name) // variant 옵션값들을 문자열 리터럴로 감싸기 acc.variants[sanitizedName] = diff --git a/src/codegen/render/index.ts b/src/codegen/render/index.ts index 1985df3..23aba08 100644 --- a/src/codegen/render/index.ts +++ b/src/codegen/render/index.ts @@ -42,17 +42,23 @@ export function renderComponent( code: string, variants: Record, ) { - const hasVariants = Object.keys(variants).length > 0 + console.log('너니') + // Filter out effect variant (treated as reserved property like viewport) + const filteredVariants = Object.fromEntries( + Object.entries(variants).filter(([key]) => key.toLowerCase() !== 'effect'), + ) + const hasVariants = Object.keys(filteredVariants).length > 0 const interfaceCode = hasVariants ? `export interface ${component}Props { -${Object.entries(variants) +${Object.entries(filteredVariants) .map(([key, value]) => ` ${key}: ${value}`) .join('\n')} }\n\n` : '' const propsParam = hasVariants - ? `{ ${Object.keys(variants).join(', ')} }: ${component}Props` + ? `{ ${Object.keys(filteredVariants).join(', ')} }: ${component}Props` : '' + console.log('나니') return `${interfaceCode}export function ${component}(${propsParam}) { return ${ code.includes('\n') diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 2279621..941aa05 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -357,14 +357,20 @@ export class ResponsiveCodegen { componentSet.componentPropertyDefinitions, ).find((key) => key.toLowerCase() === 'viewport') - // Get all variant keys excluding viewport + // Find effect variant key + const effectKey = Object.keys( + componentSet.componentPropertyDefinitions, + ).find((key) => key.toLowerCase() === 'effect') + + // Get all variant keys excluding viewport and effect const otherVariantKeys: string[] = [] const variants: Record = {} for (const [name, definition] of Object.entries( componentSet.componentPropertyDefinitions, )) { if (definition.type === 'VARIANT') { - if (name.toLowerCase() !== 'viewport') { + const lowerName = name.toLowerCase() + if (lowerName !== 'viewport') { otherVariantKeys.push(name) variants[name] = definition.variantOptions?.map((opt) => `'${opt}'`).join(' | ') || @@ -373,6 +379,11 @@ export class ResponsiveCodegen { } } + // If effect variant only, skip component rendering (effect is pseudo-selector) + if (effectKey && !viewportKey && otherVariantKeys.length === 0) { + return [] + } + // If no viewport variant, just handle other variants if (!viewportKey) { return ResponsiveCodegen.generateNonViewportVariantComponents( From c4f485c89dbfd853dea52e5111e91c5359b1fb2f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:45:51 +0900 Subject: [PATCH 29/51] Rm log --- src/codegen/render/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/codegen/render/index.ts b/src/codegen/render/index.ts index 23aba08..955ae6d 100644 --- a/src/codegen/render/index.ts +++ b/src/codegen/render/index.ts @@ -42,7 +42,6 @@ export function renderComponent( code: string, variants: Record, ) { - console.log('너니') // Filter out effect variant (treated as reserved property like viewport) const filteredVariants = Object.fromEntries( Object.entries(variants).filter(([key]) => key.toLowerCase() !== 'effect'), @@ -58,7 +57,6 @@ ${Object.entries(filteredVariants) const propsParam = hasVariants ? `{ ${Object.keys(filteredVariants).join(', ')} }: ${component}Props` : '' - console.log('나니') return `${interfaceCode}export function ${component}(${propsParam}) { return ${ code.includes('\n') From edbe49044e095873a50de48ad18feaa53f2e3a30 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:46:59 +0900 Subject: [PATCH 30/51] Add list testcase --- src/codegen/__tests__/codegen.test.ts | 155 ++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 20b5b2b..54ca33e 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -28025,6 +28025,161 @@ describe('render real world component', () => { }, ], }, + { + expected: ` +
  • + 어떤 증상이 가장 힘들고 일상생활에 영향을 주나요? +
  • +
  • + 증상이 언제부터 시작되었나요? +
  • +
  • + 증상이 시간이 지나면서 어떻게 변했나요? +
  • +
  • + 스트레스나 상황에 따라 증상이 달라지나요? +
  • +
    `, + nodes: [ + { + id: '387:1575', + name: '어떤 증상이 가장 힘들고 일상생활에 영향을 주나요? 증상이 언제부터 시작되었나요? 증상이 시간이 지나면서 어떻게 변했나요? 스트레스나 상황에 따라 증상이 달라지나요?', + type: 'TEXT', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '520:2035', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + textAutoResize: 'WIDTH_AND_HEIGHT', + strokes: [], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.21568627655506134, + g: 0.2549019753932953, + b: 0.3176470696926117, + }, + boundVariables: { + color: + '[NodeId: VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65]', + }, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + characters: + '어떤 증상이 가장 힘들고 일상생활에 영향을 주나요?\n증상이 언제부터 시작되었나요?\n증상이 시간이 지나면서 어떻게 변했나요?\n스트레스나 상황에 따라 증상이 달라지나요?', + textAlignHorizontal: 'LEFT', + isAsset: false, + textTruncation: 'DISABLED', + effects: [], + rotation: 0, + reactions: [], + visible: true, + width: 312, + height: 120, + layoutAlign: 'INHERIT', + layoutGrow: 0, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontSize: 15, + fontWeight: 400, + lineHeight: { + unit: 'PERCENT', + value: 200, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAlignVertical: 'CENTER', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: + '어떤 증상이 가장 힘들고 일상생활에 영향을 주나요?\n증상이 언제부터 시작되었나요?\n증상이 시간이 지나면서 어떻게 변했나요?\n스트레스나 상황에 따라 증상이 달라지나요?', + start: 0, + end: 95, + fontSize: 15, + fontName: { + family: 'Pretendard', + style: 'Regular', + }, + fontWeight: 400, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 200, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.21568627655506134, + g: 0.2549019753932953, + b: 0.3176470696926117, + }, + boundVariables: { + color: + '[NodeId: VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65]', + }, + }, + ], + textStyleId: 'S:52e722002759e248c2864a7e8ac3a317bda530d6,723:121', + fillStyleId: '', + listOptions: { + type: 'UNORDERED', + }, + indentation: 1, + hyperlink: null, + }, + ], + }, + { + id: '520:2035', + name: 'List', + type: 'SECTION', + children: ['387:1575'], + }, + ], + variables: [ + { + id: 'VariableID:1500634352fc7d420979f2b85b3736d4b0a410de/14:65', + name: 'text', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], From 347834cdca7affd0bda1115181a2773bbcbc60ba Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:48:08 +0900 Subject: [PATCH 31/51] Add debug flag --- src/code-impl.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index 60d5dc2..13c7772 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -114,11 +114,12 @@ function generatePowerShellCLI( return commands.join('\n') } +const debug = false + export function registerCodegen(ctx: typeof figma) { if (ctx.editorType === 'dev' && ctx.mode === 'codegen') { ctx.codegen.on('generate', async ({ node: n, language }) => { - const node = nodeProxyTracker.wrap(n) - // const node = n + const node = debug ? nodeProxyTracker.wrap(n) : n switch (language) { case 'devup-ui': { const time = Date.now() @@ -164,9 +165,11 @@ export function registerCodegen(ctx: typeof figma) { console.error('[responsive] Error generating responsive code:', e) } } - console.log( - await nodeProxyTracker.toTestCaseFormatWithVariables(node.id), - ) + if (debug) { + console.log( + await nodeProxyTracker.toTestCaseFormatWithVariables(node.id), + ) + } return [ ...(node.type === 'COMPONENT' || From 6ec242db1b48a520709ddad41f72c83aa2dddae6 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:53:41 +0900 Subject: [PATCH 32/51] Fix indentation in conditional rendering blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When rendering conditional expressions like {status === "default" && (...)}, the child elements inside the parentheses now have proper indentation. Previously, the children were rendered at the same depth as the parent, but they should be indented one additional level. Added paddingLeftMultiline to format multi-line child code with correct indentation depth (depth + 1) inside conditional blocks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- src/codegen/responsive/ResponsiveCodegen.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 941aa05..530bc9e 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -1,13 +1,14 @@ import { Codegen } from '../Codegen' import { renderComponent, renderNode } from '../render' import type { NodeTree, Props } from '../types' +import { paddingLeftMultiline } from '../utils/padding-left-multiline' import { type BreakpointKey, getBreakpointByWidth, mergePropsToResponsive, mergePropsToVariant, viewportToBreakpoint, -} from './index' +} from '.' /** * Generate responsive code by merging children inside a Section. @@ -766,8 +767,11 @@ export class ResponsiveCodegen { const childTree = childByVariant.get(onlyVariant) if (!childTree) continue const childCode = Codegen.renderTree(childTree, 0) + const formattedChildCode = childCode.includes('\n') + ? `(\n${paddingLeftMultiline(childCode, depth + 1)}\n${' '.repeat((depth + 1) * 2)})` + : childCode childrenCodes.push( - `{${variantKey} === "${onlyVariant}" && ${childCode.includes('\n') ? `(\n${childCode}\n)` : childCode}}`, + `{${variantKey} === "${onlyVariant}" && ${formattedChildCode}}`, ) } else { // Multiple (but not all) variants have this child @@ -780,9 +784,10 @@ export class ResponsiveCodegen { childByVariant, 0, ) - childrenCodes.push( - `{(${conditions}) && ${childCode.includes('\n') ? `(\n${childCode}\n)` : childCode}}`, - ) + const formattedChildCode = childCode.includes('\n') + ? `(\n${paddingLeftMultiline(childCode, depth + 1)}\n${' '.repeat((depth + 1) * 2)})` + : childCode + childrenCodes.push(`{(${conditions}) && ${formattedChildCode}}`) } } } From cfd783ecbc35cb093918442803ed1f0347c42564 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 01:59:47 +0900 Subject: [PATCH 33/51] Fix space --- src/codegen/responsive/ResponsiveCodegen.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 530bc9e..6a49d00 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -768,10 +768,10 @@ export class ResponsiveCodegen { if (!childTree) continue const childCode = Codegen.renderTree(childTree, 0) const formattedChildCode = childCode.includes('\n') - ? `(\n${paddingLeftMultiline(childCode, depth + 1)}\n${' '.repeat((depth + 1) * 2)})` + ? `(\n${paddingLeftMultiline(childCode, depth)}\n )` : childCode childrenCodes.push( - `{${variantKey} === "${onlyVariant}" && ${formattedChildCode}}`, + ` {${variantKey} === "${onlyVariant}" && ${formattedChildCode}}`, ) } else { // Multiple (but not all) variants have this child @@ -785,7 +785,7 @@ export class ResponsiveCodegen { 0, ) const formattedChildCode = childCode.includes('\n') - ? `(\n${paddingLeftMultiline(childCode, depth + 1)}\n${' '.repeat((depth + 1) * 2)})` + ? `(\n${paddingLeftMultiline(childCode, depth)}\n )` : childCode childrenCodes.push(`{(${conditions}) && ${formattedChildCode}}`) } From 162f786945872defd59aa8b591104c7e25890a36 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 02:04:59 +0900 Subject: [PATCH 34/51] Fix space --- src/codegen/responsive/ResponsiveCodegen.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 6a49d00..b3f0b32 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -768,10 +768,10 @@ export class ResponsiveCodegen { if (!childTree) continue const childCode = Codegen.renderTree(childTree, 0) const formattedChildCode = childCode.includes('\n') - ? `(\n${paddingLeftMultiline(childCode, depth)}\n )` + ? `(\n${paddingLeftMultiline(childCode, 1)}\n)` : childCode childrenCodes.push( - ` {${variantKey} === "${onlyVariant}" && ${formattedChildCode}}`, + `{${variantKey} === "${onlyVariant}" && ${formattedChildCode}}`, ) } else { // Multiple (but not all) variants have this child @@ -785,7 +785,7 @@ export class ResponsiveCodegen { 0, ) const formattedChildCode = childCode.includes('\n') - ? `(\n${paddingLeftMultiline(childCode, depth)}\n )` + ? `2(\n${paddingLeftMultiline(childCode, 1)}\n)` : childCode childrenCodes.push(`{(${conditions}) && ${formattedChildCode}}`) } From 1d9577377608ab98cb465335fd625978e55fae3a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 17:23:52 +0900 Subject: [PATCH 35/51] Implement responsive --- src/code-impl.ts | 2 +- src/codegen/props/selector.ts | 77 ++- src/codegen/responsive/ResponsiveCodegen.ts | 508 ++++++++++++++++-- .../__tests__/ResponsiveCodegen.test.ts | 152 ++++++ .../ResponsiveCodegen.test.ts.snap | 2 + src/codegen/responsive/index.ts | 50 +- src/codegen/utils/props-to-str.ts | 53 +- 7 files changed, 806 insertions(+), 38 deletions(-) diff --git a/src/code-impl.ts b/src/code-impl.ts index 13c7772..bf99f2b 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -114,7 +114,7 @@ function generatePowerShellCLI( return commands.join('\n') } -const debug = false +const debug = true export function registerCodegen(ctx: typeof figma) { if (ctx.editorType === 'dev' && ctx.mode === 'codegen') { diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index 98cb2f6..403a7c7 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -48,7 +48,7 @@ export async function getSelectorProps( [ hasEffect ? component.variantProperties?.effect - : triggerTypeToEffect(component.reactions[0]?.trigger?.type), + : triggerTypeToEffect(component.reactions?.[0]?.trigger?.type), await getProps(component), ] as const, ), @@ -83,7 +83,8 @@ export async function getSelectorProps( .flat()[0] const diffKeys = new Set() for (const [effect, props] of components) { - if (!effect) continue + // Skip if no effect or if effect is "default" (default state doesn't need pseudo-selector) + if (!effect || effect === 'default') continue const def = difference(props, defaultProps) if (Object.keys(def).length === 0) continue result.props[`_${effect}`] = def @@ -102,6 +103,78 @@ export async function getSelectorProps( return result } +/** + * Get selector props for a specific variant group (e.g., size=Md, variant=primary). + * This filters components by the given variant properties before calculating pseudo-selector diffs. + * + * @param componentSet The component set to extract selector props from + * @param variantFilter Object containing variant key-value pairs to filter by (excluding effect and viewport) + */ +export async function getSelectorPropsForGroup( + componentSet: ComponentSetNode, + variantFilter: Record, +): Promise> { + const hasEffect = !!componentSet.componentPropertyDefinitions.effect + if (!hasEffect) return {} + + // Filter components matching the variant filter + const matchingComponents = componentSet.children.filter((child) => { + if (child.type !== 'COMPONENT') return false + const variantProps = child.variantProperties || {} + + // Check all filter conditions match + for (const [key, value] of Object.entries(variantFilter)) { + if (variantProps[key] !== value) return false + } + return true + }) as ComponentNode[] + + if (matchingComponents.length === 0) return {} + + // Find the default component in this group (effect=default) + const defaultComponent = matchingComponents.find( + (c) => c.variantProperties?.effect === 'default', + ) + if (!defaultComponent) return {} + + const defaultProps = await getProps(defaultComponent) + const result: Record = {} + const diffKeys = new Set() + + // Calculate diffs for each effect state + for (const component of matchingComponents) { + const effect = component.variantProperties?.effect + if (!effect || effect === 'default') continue + + const props = await getProps(component) + const def = difference(props, defaultProps) + if (Object.keys(def).length === 0) continue + + result[`_${effect}`] = def + for (const key of Object.keys(def)) { + diffKeys.add(key) + } + } + + // Add transition if available + if (diffKeys.size > 0) { + const findNodeAction = (action: Action) => action.type === 'NODE' + const getTransition = (reaction: Reaction) => + reaction.actions?.find(findNodeAction)?.transition + const transition = defaultComponent.reactions + ?.flatMap(getTransition) + .flat()[0] + if (transition?.type === 'SMART_ANIMATE') { + const keys = Array.from(diffKeys) + keys.sort() + result.transition = `${fmtPct(transition.duration)}ms ${transition.easing.type.toLocaleLowerCase().replaceAll('_', '-')}` + result.transitionProperty = keys.join(',') + } + } + + return result +} + function triggerTypeToEffect(triggerType: Trigger['type'] | undefined) { if (!triggerType) return undefined switch (triggerType) { diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index b3f0b32..fc2ebd3 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -1,12 +1,15 @@ import { Codegen } from '../Codegen' +import { getSelectorPropsForGroup } from '../props/selector' import { renderComponent, renderNode } from '../render' import type { NodeTree, Props } from '../types' import { paddingLeftMultiline } from '../utils/padding-left-multiline' import { type BreakpointKey, + createVariantPropValue, getBreakpointByWidth, mergePropsToResponsive, mergePropsToVariant, + type PropValue, viewportToBreakpoint, } from '.' @@ -313,16 +316,42 @@ export class ResponsiveCodegen { } } + // Check if componentSet has effect variant (pseudo-selector) + const hasEffect = Object.keys( + componentSet.componentPropertyDefinitions, + ).some((key) => key.toLowerCase() === 'effect') + // Generate responsive code for each group const results: Array = [] const responsiveCodegen = new ResponsiveCodegen(null) - for (const [, viewportComponents] of groups) { + for (const [groupKey, viewportComponents] of groups) { + // Parse group key to get variant filter for getSelectorPropsForGroup + const variantFilter: Record = {} + if (groupKey !== '__default__') { + for (const part of groupKey.split('|')) { + const [key, value] = part.split('=') + // Exclude effect from filter (we want all effect variants for this group) + if (key.toLowerCase() !== 'effect') { + variantFilter[key] = value + } + } + } + + // Get pseudo-selector props for this specific variant group + const selectorProps = hasEffect + ? await getSelectorPropsForGroup(componentSet, variantFilter) + : null + // Build trees for each viewport const treesByBreakpoint = new Map() for (const [bp, component] of viewportComponents) { const codegen = new Codegen(component) const tree = await codegen.getTree() + // Add pseudo-selector props to tree + if (selectorProps && Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } treesByBreakpoint.set(bp, tree) } @@ -371,7 +400,10 @@ export class ResponsiveCodegen { )) { if (definition.type === 'VARIANT') { const lowerName = name.toLowerCase() - if (lowerName !== 'viewport') { + // Exclude both viewport and effect from variant keys + // viewport is handled by responsive merging + // effect is handled by getSelectorProps (pseudo-selectors like _hover, _active) + if (lowerName !== 'viewport' && lowerName !== 'effect') { otherVariantKeys.push(name) variants[name] = definition.variantOptions?.map((opt) => `'${opt}'`).join(' | ') || @@ -404,12 +436,34 @@ export class ResponsiveCodegen { } // Handle both viewport and other variants - // For simplicity, use the first non-viewport variant key - const primaryVariantKey = otherVariantKeys[0] + // Group by ALL variant keys combined, then by viewport within each group + // e.g., for size+variant: { "Md|primary" => { "mobile" => Component, "pc" => Component }, ... } + + // Build a composite key from all variant values + const buildCompositeKey = ( + variantProps: Record, + ): string => { + return otherVariantKeys + .map((key) => `${key}=${variantProps[key] || '__default__'}`) + .join('|') + } - // Group by variant value first, then by viewport within each group - // e.g., { "default" => { "mobile" => Component, "pc" => Component }, "scroll" => { ... } } - const byVariantValue = new Map>() + // Parse composite key back to variant values + const parseCompositeKey = ( + compositeKey: string, + ): Record => { + const result: Record = {} + for (const part of compositeKey.split('|')) { + const [key, value] = part.split('=') + result[key] = value + } + return result + } + + const byCompositeVariant = new Map< + string, + Map + >() for (const child of componentSet.children) { if (child.type !== 'COMPONENT') continue @@ -417,60 +471,60 @@ export class ResponsiveCodegen { const component = child as ComponentNode const variantProps = component.variantProperties || {} + // Skip effect variants for grouping (they become pseudo-selectors) + if (effectKey && variantProps[effectKey] !== 'default') continue + const viewportValue = variantProps[viewportKey] if (!viewportValue) continue const breakpoint = viewportToBreakpoint(viewportValue) - const variantValue = variantProps[primaryVariantKey] || '__default__' + const compositeKey = buildCompositeKey(variantProps) - if (!byVariantValue.has(variantValue)) { - byVariantValue.set(variantValue, new Map()) + if (!byCompositeVariant.has(compositeKey)) { + byCompositeVariant.set(compositeKey, new Map()) } - const byBreakpoint = byVariantValue.get(variantValue) + const byBreakpoint = byCompositeVariant.get(compositeKey) if (byBreakpoint) { byBreakpoint.set(breakpoint, component) } } - if (byVariantValue.size === 0) { + if (byCompositeVariant.size === 0) { return [] } const responsiveCodegen = new ResponsiveCodegen(null) - // Step 1: For each variant value, merge by viewport to get responsive props - const mergedTreesByVariant = new Map() - const responsivePropsByVariant = new Map< + // Step 1: For each variant combination, merge by viewport to get responsive props + const responsivePropsByComposite = new Map< string, Map >() - for (const [variantValue, viewportComponents] of byVariantValue) { + for (const [compositeKey, viewportComponents] of byCompositeVariant) { + // Get pseudo-selector props for this specific variant group + const variantFilter = parseCompositeKey(compositeKey) + const selectorProps = effectKey + ? await getSelectorPropsForGroup(componentSet, variantFilter) + : null + const treesByBreakpoint = new Map() for (const [bp, component] of viewportComponents) { const codegen = new Codegen(component) const tree = await codegen.getTree() + // Add pseudo-selector props to tree + if (selectorProps && Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } treesByBreakpoint.set(bp, tree) } - responsivePropsByVariant.set(variantValue, treesByBreakpoint) - - // Get merged tree with responsive props - const firstTree = [...treesByBreakpoint.values()][0] - const propsMap = new Map() - for (const [bp, tree] of treesByBreakpoint) { - propsMap.set(bp, tree.props) - } - const mergedProps = mergePropsToResponsive(propsMap) - mergedTreesByVariant.set(variantValue, { - ...firstTree, - props: mergedProps, - }) + responsivePropsByComposite.set(compositeKey, treesByBreakpoint) } - // Step 2: Merge across variant values to create conditional props - const mergedCode = responsiveCodegen.generateVariantMergedCode( - primaryVariantKey, - responsivePropsByVariant, + // Step 2: Merge across variant values, handling multiple variant keys + const mergedCode = responsiveCodegen.generateMultiVariantMergedCode( + otherVariantKeys, + responsivePropsByComposite, 2, ) @@ -509,11 +563,28 @@ export class ResponsiveCodegen { } } + // Check if componentSet has effect variant (pseudo-selector) + const hasEffect = Object.keys( + componentSet.componentPropertyDefinitions, + ).some((key) => key.toLowerCase() === 'effect') + // Build trees for each variant const treesByVariant = new Map() for (const [variantValue, component] of componentsByVariant) { + // Get pseudo-selector props for this specific variant group + const variantFilter: Record = { + [primaryVariantKey]: variantValue, + } + const selectorProps = hasEffect + ? await getSelectorPropsForGroup(componentSet, variantFilter) + : null + const codegen = new Codegen(component) const tree = await codegen.getTree() + // Add pseudo-selector props to tree + if (selectorProps && Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } treesByVariant.set(variantValue, tree) } @@ -796,4 +867,375 @@ export class ResponsiveCodegen { return renderNode(firstTree.component, mergedProps, depth, childrenCodes) } + + /** + * Generate merged code from NodeTree objects across multiple variant dimensions and viewport. + * Handles composite keys like "size=Md|variant=primary" and produces nested variant conditionals. + * + * For props that differ: + * - Across viewport only: responsive array ["mobile", null, null, null, "desktop"] + * - Across one variant only: { Md: "value1", Sm: "value2" }[size] + * - Across both: { Md: ["mobile", null, null, null, "desktop"], Sm: "fixed" }[size] + * - Across multiple variants: nested conditionals or combined + */ + generateMultiVariantMergedCode( + variantKeys: string[], + treesByCompositeAndBreakpoint: Map>, + depth: number, + ): string { + // First, for each composite variant, merge across breakpoints + const mergedTreesByComposite = new Map() + + for (const [ + compositeKey, + treesByBreakpoint, + ] of treesByCompositeAndBreakpoint) { + const firstTree = [...treesByBreakpoint.values()][0] + const propsMap = new Map() + for (const [bp, tree] of treesByBreakpoint) { + propsMap.set(bp, tree.props) + } + const mergedProps = mergePropsToResponsive(propsMap) + + // Also merge children recursively + const mergedChildren = + this.mergeChildrenAcrossBreakpoints(treesByBreakpoint) + + mergedTreesByComposite.set(compositeKey, { + ...firstTree, + props: mergedProps, + children: mergedChildren, + }) + } + + // Now merge across all variant dimensions + // We'll process each variant key in sequence, merging values + return this.generateNestedVariantMergedCode( + variantKeys, + mergedTreesByComposite, + depth, + ) + } + + /** + * Generate merged code with nested variant conditionals. + * For multiple variant keys, this creates props like: + * { primary: { Md: [...], Sm: [...] }[size], white: { Md: [...], Sm: [...] }[size] }[variant] + */ + private generateNestedVariantMergedCode( + variantKeys: string[], + treesByComposite: Map, + depth: number, + ): string { + const firstTree = [...treesByComposite.values()][0] + + // Build props map indexed by composite key + const propsMap = new Map>() + for (const [compositeKey, tree] of treesByComposite) { + propsMap.set(compositeKey, tree.props) + } + + // Merge props across all composite variants + const mergedProps = this.mergePropsAcrossComposites(variantKeys, propsMap) + + // Handle TEXT nodes + if (firstTree.textChildren && firstTree.textChildren.length > 0) { + return renderNode( + firstTree.component, + mergedProps, + depth, + firstTree.textChildren, + ) + } + + // For children, we need to merge across all composite variants + const childrenCodes: string[] = [] + + // Build children maps for each composite variant + const childrenMaps = new Map>() + for (const [compositeKey, tree] of treesByComposite) { + childrenMaps.set(compositeKey, this.treeChildrenToMap(tree)) + } + + // Get all unique child names + const processedChildNames = new Set() + const allChildNames: string[] = [] + const firstComposite = [...treesByComposite.keys()][0] + const firstChildrenMap = childrenMaps.get(firstComposite) + + if (firstChildrenMap) { + for (const name of firstChildrenMap.keys()) { + allChildNames.push(name) + processedChildNames.add(name) + } + } + + for (const childMap of childrenMaps.values()) { + for (const name of childMap.keys()) { + if (!processedChildNames.has(name)) { + allChildNames.push(name) + processedChildNames.add(name) + } + } + } + + // Process each child + const allCompositeKeys = [...treesByComposite.keys()] + + for (const childName of allChildNames) { + let maxChildCount = 0 + for (const childMap of childrenMaps.values()) { + const children = childMap.get(childName) + if (children) { + maxChildCount = Math.max(maxChildCount, children.length) + } + } + + for (let childIndex = 0; childIndex < maxChildCount; childIndex++) { + const childByComposite = new Map() + const presentComposites = new Set() + + for (const [compositeKey, childMap] of childrenMaps) { + const children = childMap.get(childName) + if (children && children.length > childIndex) { + childByComposite.set(compositeKey, children[childIndex]) + presentComposites.add(compositeKey) + } + } + + if (childByComposite.size > 0) { + const existsInAll = allCompositeKeys.every((k) => + presentComposites.has(k), + ) + + if (existsInAll) { + // Child exists in all variants - recursively merge + const childCode = this.generateNestedVariantMergedCode( + variantKeys, + childByComposite, + 0, + ) + childrenCodes.push(childCode) + } else { + // Child exists only in some variants - use first one for now + // TODO: implement conditional rendering for partial children + const firstChildTree = [...childByComposite.values()][0] + const childCode = Codegen.renderTree(firstChildTree, 0) + childrenCodes.push(childCode) + } + } + } + } + + return renderNode(firstTree.component, mergedProps, depth, childrenCodes) + } + + /** + * Merge props across composite variant keys. + * Creates nested variant conditionals for props that differ. + */ + private mergePropsAcrossComposites( + variantKeys: string[], + propsMap: Map>, + ): Record { + const result: Record = {} + + // Collect all prop keys + const allPropKeys = new Set() + for (const props of propsMap.values()) { + for (const key of Object.keys(props)) { + allPropKeys.add(key) + } + } + + // For each prop, determine how to merge it + for (const propKey of allPropKeys) { + // Check if prop is a pseudo-selector (needs special handling) + if (propKey.startsWith('_')) { + // Collect pseudo-selector props across all composites + const pseudoPropsMap = new Map>() + for (const [compositeKey, props] of propsMap) { + if ( + propKey in props && + typeof props[propKey] === 'object' && + props[propKey] !== null + ) { + pseudoPropsMap.set( + compositeKey, + props[propKey] as Record, + ) + } + } + if (pseudoPropsMap.size > 0) { + result[propKey] = this.mergePropsAcrossComposites( + variantKeys, + pseudoPropsMap, + ) + } + continue + } + + // Collect values for this prop across all composites + const valuesByComposite = new Map() + for (const [compositeKey, props] of propsMap) { + if (propKey in props) { + valuesByComposite.set(compositeKey, props[propKey]) + } + } + + // Check if all values are the same + const uniqueValues = new Set() + for (const value of valuesByComposite.values()) { + uniqueValues.add(JSON.stringify(value)) + } + + if (uniqueValues.size === 1) { + // All values are the same - use as-is + result[propKey] = [...valuesByComposite.values()][0] + } else { + // Values differ - need to create variant conditional + // Try to find which variant key causes the difference + result[propKey] = this.createNestedVariantProp( + variantKeys, + valuesByComposite, + ) + } + } + + return result + } + + /** + * Create a nested variant prop value for props that differ across multiple variant dimensions. + */ + private createNestedVariantProp( + variantKeys: string[], + valuesByComposite: Map, + ): unknown { + // Parse composite keys + const parseCompositeKey = ( + compositeKey: string, + ): Record => { + const parsed: Record = {} + for (const part of compositeKey.split('|')) { + const [key, value] = part.split('=') + parsed[key] = value + } + return parsed + } + + // If only one variant key, create simple conditional + if (variantKeys.length === 1) { + const variantKey = variantKeys[0] + const valuesByVariant: Record = {} + + for (const [compositeKey, value] of valuesByComposite) { + const parsed = parseCompositeKey(compositeKey) + const variantValue = parsed[variantKey] + valuesByVariant[variantValue] = value + } + + // Check if all values are the same + const uniqueValues = new Set( + Object.values(valuesByVariant).map((v) => JSON.stringify(v)), + ) + if (uniqueValues.size === 1) { + return Object.values(valuesByVariant)[0] + } + + return createVariantPropValue( + variantKey, + valuesByVariant as Record, + ) + } + + // For multiple variant keys, we need to determine which key(s) cause the difference + // and create nested conditionals + + // Try each variant key to see if it alone explains the difference + for (const variantKey of variantKeys) { + const valuesByVariant = new Map>() + + for (const [compositeKey, value] of valuesByComposite) { + const parsed = parseCompositeKey(compositeKey) + const variantValue = parsed[variantKey] + + if (!valuesByVariant.has(variantValue)) { + valuesByVariant.set(variantValue, new Map()) + } + // Build a sub-composite key without this variant + const subCompositeKey = variantKeys + .filter((k) => k !== variantKey) + .map((k) => `${k}=${parsed[k]}`) + .join('|') + const variantMap = valuesByVariant.get(variantValue) + if (variantMap) { + variantMap.set(subCompositeKey, value) + } + } + + // Check if this variant key alone explains the difference + // (all values within each variant value are the same) + let variantExplainsAll = true + const simplifiedValues: Record = {} + + for (const [variantValue, subValues] of valuesByVariant) { + const uniqueSubValues = new Set( + [...subValues.values()].map((v) => JSON.stringify(v)), + ) + if (uniqueSubValues.size === 1) { + simplifiedValues[variantValue] = [...subValues.values()][0] + } else { + variantExplainsAll = false + break + } + } + + if (variantExplainsAll) { + // This variant key alone explains the difference + return createVariantPropValue( + variantKey, + simplifiedValues as Record, + ) + } + } + + // Multiple variant keys contribute to the difference + // Use the first variant key and recurse for nested conditionals + const primaryKey = variantKeys[0] + const remainingKeys = variantKeys.slice(1) + + const valuesByPrimaryVariant = new Map>() + + for (const [compositeKey, value] of valuesByComposite) { + const parsed = parseCompositeKey(compositeKey) + const primaryValue = parsed[primaryKey] + + if (!valuesByPrimaryVariant.has(primaryValue)) { + valuesByPrimaryVariant.set(primaryValue, new Map()) + } + + const subCompositeKey = remainingKeys + .map((k) => `${k}=${parsed[k]}`) + .join('|') + const primaryMap = valuesByPrimaryVariant.get(primaryValue) + if (primaryMap) { + primaryMap.set(subCompositeKey, value) + } + } + + // Create nested structure + const nestedValues: Record = {} + for (const [primaryValue, subValues] of valuesByPrimaryVariant) { + nestedValues[primaryValue] = this.createNestedVariantProp( + remainingKeys, + subValues, + ) + } + + return createVariantPropValue( + primaryKey, + nestedValues as Record, + ) + } } diff --git a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts index dcc7b0b..35ec8e3 100644 --- a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts +++ b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts @@ -1,6 +1,19 @@ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' import type { NodeTree } from '../../types' +// Mock figma global +;(globalThis as { figma?: unknown }).figma = { + mixed: Symbol('mixed'), + util: { + rgba: (color: { r: number; g: number; b: number; a?: number }) => ({ + r: color.r, + g: color.g, + b: color.b, + a: color.a ?? 1, + }), + }, +} as unknown as typeof figma + const renderNodeMock = mock( ( component: string, @@ -710,5 +723,144 @@ describe('ResponsiveCodegen', () => { // Should have status in interface expect(result[0][1]).toContain('status') }) + + it('handles effect + viewport + size + variant (4 dimensions)', async () => { + // Button component with: + // - effect: default, hover, active + // - viewport: Desktop, Mobile + // - size: Md, Sm + // - variant: primary, white + + const createComponent = ( + effect: string, + viewport: string, + size: string, + variant: string, + bgColor: { r: number; g: number; b: number }, + ) => + ({ + type: 'COMPONENT', + name: `effect=${effect}, viewport=${viewport}, size=${size}, variant=${variant}`, + variantProperties: { effect, viewport, size, variant }, + children: [], + layoutMode: 'HORIZONTAL', + width: viewport === 'Desktop' ? (size === 'Md' ? 191 : 123) : 149, + height: viewport === 'Desktop' ? (size === 'Md' ? 64 : 46) : 51, + fills: [ + { + type: 'SOLID', + visible: true, + color: bgColor, + opacity: 1, + }, + ], + reactions: [], + }) as unknown as ComponentNode + + // Primary variant colors + const primaryDefault = { r: 0.35, g: 0.25, b: 0.17 } + const primaryHover = { r: 0.24, g: 0.17, b: 0.12 } + const primaryActive = { r: 0.19, g: 0.14, b: 0.1 } + + // White variant colors + const whiteDefault = { r: 1, g: 1, b: 1 } + const whiteHover = { r: 0.95, g: 0.95, b: 0.95 } + const whiteActive = { r: 0.9, g: 0.9, b: 0.9 } + + const componentSet = { + type: 'COMPONENT_SET', + name: 'Button', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['active', 'default', 'hover'], + }, + viewport: { + type: 'VARIANT', + defaultValue: 'Desktop', + variantOptions: ['Desktop', 'Mobile'], + }, + size: { + type: 'VARIANT', + defaultValue: 'Md', + variantOptions: ['Md', 'Sm'], + }, + variant: { + type: 'VARIANT', + defaultValue: 'primary', + variantOptions: ['primary', 'white'], + }, + }, + children: [ + // Primary Md + createComponent( + 'default', + 'Desktop', + 'Md', + 'primary', + primaryDefault, + ), + createComponent('hover', 'Desktop', 'Md', 'primary', primaryHover), + createComponent('active', 'Desktop', 'Md', 'primary', primaryActive), + createComponent('default', 'Mobile', 'Md', 'primary', primaryDefault), + createComponent('hover', 'Mobile', 'Md', 'primary', primaryHover), + createComponent('active', 'Mobile', 'Md', 'primary', primaryActive), + // Primary Sm + createComponent( + 'default', + 'Desktop', + 'Sm', + 'primary', + primaryDefault, + ), + createComponent('hover', 'Desktop', 'Sm', 'primary', primaryHover), + createComponent('active', 'Desktop', 'Sm', 'primary', primaryActive), + createComponent('default', 'Mobile', 'Sm', 'primary', primaryDefault), + createComponent('hover', 'Mobile', 'Sm', 'primary', primaryHover), + createComponent('active', 'Mobile', 'Sm', 'primary', primaryActive), + // White Md + createComponent('default', 'Desktop', 'Md', 'white', whiteDefault), + createComponent('hover', 'Desktop', 'Md', 'white', whiteHover), + createComponent('active', 'Desktop', 'Md', 'white', whiteActive), + createComponent('default', 'Mobile', 'Md', 'white', whiteDefault), + createComponent('hover', 'Mobile', 'Md', 'white', whiteHover), + createComponent('active', 'Mobile', 'Md', 'white', whiteActive), + // White Sm + createComponent('default', 'Desktop', 'Sm', 'white', whiteDefault), + createComponent('hover', 'Desktop', 'Sm', 'white', whiteHover), + createComponent('active', 'Desktop', 'Sm', 'white', whiteActive), + createComponent('default', 'Mobile', 'Sm', 'white', whiteDefault), + createComponent('hover', 'Mobile', 'Sm', 'white', whiteHover), + createComponent('active', 'Mobile', 'Sm', 'white', whiteActive), + ], + } as unknown as ComponentSetNode + + // Set default variant + ;(componentSet as { defaultVariant: ComponentNode }).defaultVariant = + componentSet.children[0] as ComponentNode + + const result = + await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'Button', + ) + + expect(result.length).toBe(1) + expect(result[0][0]).toBe('Button') + + const code = result[0][1] + + // Should have size and variant in interface (not effect, not viewport) + expect(code).toContain('size') + expect(code).toContain('variant') + // effect should NOT be in the interface (handled as pseudo-selectors) + expect(code).not.toMatch(/effect:\s*['"]/) + // viewport should NOT be in the interface (handled as responsive arrays) + expect(code).not.toMatch(/viewport:\s*['"]/) + + // Verify snapshot for the generated code + expect(code).toMatchSnapshot() + }) }) }) diff --git a/src/codegen/responsive/__tests__/__snapshots__/ResponsiveCodegen.test.ts.snap b/src/codegen/responsive/__tests__/__snapshots__/ResponsiveCodegen.test.ts.snap index f0b7828..2a71efb 100644 --- a/src/codegen/responsive/__tests__/__snapshots__/ResponsiveCodegen.test.ts.snap +++ b/src/codegen/responsive/__tests__/__snapshots__/ResponsiveCodegen.test.ts.snap @@ -1,3 +1,5 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`ResponsiveCodegen generateVariantOnlyMergedCode renders OR conditional for child existing in multiple but not all variants 1`] = `"render:Flex:depth=0:{}|{(status === "scroll" || status === "hover") && render:Box:depth=0:{"id":{"__variantProp":true,"variantKey":"status","values":{"scroll":"PartialChild","hover":"PartialChildHover"}}}|}"`; + +exports[`ResponsiveCodegen generateVariantResponsiveComponents handles effect + viewport + size + variant (4 dimensions) 1`] = `"component:Button:{"size":"'Md' | 'Sm'","variant":"'primary' | 'white'"}|render:Box:depth=2:{"id":"RootTablet","_hover":{"bg":{"__variantProp":true,"variantKey":"variant","values":{"primary":"#3D2B1F","white":"#F2F2F2"}}},"_active":{"bg":{"__variantProp":true,"variantKey":"variant","values":{"primary":"#30241A","white":"#E6E6E6"}}}}|render:Box:depth=0:{"id":"Shared"}|"`; diff --git a/src/codegen/responsive/index.ts b/src/codegen/responsive/index.ts index 20c1224..b731392 100644 --- a/src/codegen/responsive/index.ts +++ b/src/codegen/responsive/index.ts @@ -63,7 +63,7 @@ export function groupChildrenByBreakpoint( return groups } -type PropValue = boolean | string | number | undefined | null | object +export type PropValue = boolean | string | number | undefined | null | object export type Props = Record const SPECIAL_PROPS_WITH_INITIAL = new Set([ 'display', @@ -200,6 +200,13 @@ export function optimizeResponsiveValue( * Merge props across breakpoints into responsive arrays. * Always 5 slots: [mobile, sm, tablet, lg, pc]; trailing nulls trimmed. */ +/** + * Check if a prop key is a pseudo-selector prop (e.g., _hover, _active, _disabled, _focus). + */ +function isPseudoSelectorProp(key: string): boolean { + return key.startsWith('_') +} + export function mergePropsToResponsive( breakpointProps: Map>, ): Record { @@ -220,6 +227,27 @@ export function mergePropsToResponsive( } for (const key of allKeys) { + // Pseudo-selector props (e.g., _hover, _active, _disabled) need special handling: + // Their inner props should be merged into responsive arrays + if (isPseudoSelectorProp(key)) { + // Collect pseudo-selector objects from each breakpoint + const pseudoPropsMap = new Map>() + for (const [bp, props] of breakpointProps) { + if ( + key in props && + typeof props[key] === 'object' && + props[key] !== null + ) { + pseudoPropsMap.set(bp, props[key] as Record) + } + } + if (pseudoPropsMap.size > 0) { + // Recursively merge the inner props of pseudo-selector + result[key] = mergePropsToResponsive(pseudoPropsMap) + } + continue + } + // Collect values for 5 fixed slots. const values: (PropValue | null)[] = BREAKPOINT_ORDER.map((bp) => { const props = breakpointProps.get(bp) @@ -390,6 +418,26 @@ export function mergePropsToVariant( } for (const key of allKeys) { + // Pseudo-selector props (e.g., _hover, _active, _disabled) need special handling: + // Their inner props should be merged with variant conditionals + if (isPseudoSelectorProp(key)) { + // Collect pseudo-selector objects from each variant + const pseudoPropsMap = new Map>() + for (const [variant, props] of variantProps) { + if ( + key in props && + typeof props[key] === 'object' && + props[key] !== null + ) { + pseudoPropsMap.set(variant, props[key] as Record) + } + } + if (pseudoPropsMap.size > 0) { + // Recursively merge the inner props of pseudo-selector + result[key] = mergePropsToVariant(variantKey, pseudoPropsMap) + } + continue + } // Collect values for each variant. const valuesByVariant: Record = {} let hasValue = false diff --git a/src/codegen/utils/props-to-str.ts b/src/codegen/utils/props-to-str.ts index 6ff7494..85d34ca 100644 --- a/src/codegen/utils/props-to-str.ts +++ b/src/codegen/utils/props-to-str.ts @@ -2,7 +2,7 @@ import { isVariantPropValue } from '../responsive' /** * Convert a value to its JSX string representation. - * Handles primitives, arrays, and objects. + * Handles primitives, arrays, objects, and VariantPropValue. */ function valueToJsxString(value: unknown): string { if (value === null) return 'null' @@ -15,6 +15,10 @@ function valueToJsxString(value: unknown): string { const items = value.map((item) => valueToJsxString(item)) return `[${items.join(', ')}]` } + // Handle VariantPropValue inside objects + if (isVariantPropValue(value)) { + return formatVariantPropValue(value) + } if (typeof value === 'object') { return JSON.stringify(value) } @@ -35,6 +39,49 @@ function formatVariantPropValue(variantProp: { return `{ ${parts.join(', ')} }[${variantProp.variantKey}]` } +/** + * Convert an object to JSX string, handling nested VariantPropValue. + * Uses JSON.stringify-like formatting but replaces VariantPropValue with proper syntax. + */ +function objectToJsxString( + obj: Record, + indent: number = 0, +): string { + const entries = Object.entries(obj) + const spaces = ' '.repeat(indent + 1) + const closingSpaces = ' '.repeat(indent) + + const parts = entries.map(([key, value]) => { + const formattedValue = formatObjectValue(value, indent + 1) + return `${spaces}"${key}": ${formattedValue}` + }) + + return `{\n${parts.join(',\n')}\n${closingSpaces}}` +} + +/** + * Format a value inside an object, handling nested objects and VariantPropValue. + */ +function formatObjectValue(value: unknown, indent: number): string { + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (typeof value === 'string') return `"${value}"` + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + if (Array.isArray(value)) { + const items = value.map((item) => valueToJsxString(item)) + return `[${items.join(', ')}]` + } + if (isVariantPropValue(value)) { + return formatVariantPropValue(value) + } + if (typeof value === 'object') { + return objectToJsxString(value as Record, indent) + } + return String(value) +} + export function propsToString(props: Record) { const sorted = Object.entries(props).sort((a, b) => { const isAUpper = /^[A-Z]/.test(a[0]) @@ -50,6 +97,10 @@ export function propsToString(props: Record) { if (isVariantPropValue(value)) { return `${key}={${formatVariantPropValue(value)}}` } + // Handle pseudo-selector props (e.g., _hover, _active) which may contain VariantPropValue + if (typeof value === 'object' && value !== null && key.startsWith('_')) { + return `${key}={${objectToJsxString(value as Record)}}` + } if (typeof value === 'object') return `${key}={${JSON.stringify(value, null, 2)}}` // Special handling for animationName with keyframes function From 23676534516cc110f685f1b76589fe9396f31c7e Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 17:40:09 +0900 Subject: [PATCH 36/51] Implement responsive --- src/codegen/props/selector.ts | 15 +- src/codegen/responsive/ResponsiveCodegen.ts | 178 ++++++++------ .../__tests__/ResponsiveCodegen.test.ts | 219 ++++++++++++++++++ 3 files changed, 339 insertions(+), 73 deletions(-) diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index 403a7c7..2e9946d 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -109,15 +109,22 @@ export async function getSelectorProps( * * @param componentSet The component set to extract selector props from * @param variantFilter Object containing variant key-value pairs to filter by (excluding effect and viewport) + * @param viewportValue Optional viewport value to filter by (e.g., 'Desktop', 'Mobile') */ export async function getSelectorPropsForGroup( componentSet: ComponentSetNode, variantFilter: Record, + viewportValue?: string, ): Promise> { const hasEffect = !!componentSet.componentPropertyDefinitions.effect if (!hasEffect) return {} - // Filter components matching the variant filter + // Find viewport key if needed + const viewportKey = Object.keys( + componentSet.componentPropertyDefinitions, + ).find((key) => key.toLowerCase() === 'viewport') + + // Filter components matching the variant filter (and viewport if specified) const matchingComponents = componentSet.children.filter((child) => { if (child.type !== 'COMPONENT') return false const variantProps = child.variantProperties || {} @@ -126,6 +133,12 @@ export async function getSelectorPropsForGroup( for (const [key, value] of Object.entries(variantFilter)) { if (variantProps[key] !== value) return false } + + // Check viewport if specified + if (viewportValue && viewportKey) { + if (variantProps[viewportKey] !== viewportValue) return false + } + return true }) as ComponentNode[] diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index fc2ebd3..5056289 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -285,7 +285,12 @@ export class ResponsiveCodegen { } } - // Group components by non-viewport variants + // Find effect variant key (to exclude from grouping) + const effectKey = Object.keys( + componentSet.componentPropertyDefinitions, + ).find((key) => key.toLowerCase() === 'effect') + + // Group components by non-viewport, non-effect variants const groups = new Map>() for (const child of componentSet.children) { @@ -294,13 +299,19 @@ export class ResponsiveCodegen { const component = child as ComponentNode const variantProps = component.variantProperties || {} + // Skip non-default effect variants (they become pseudo-selectors) + if (effectKey && variantProps[effectKey] !== 'default') continue + const viewportValue = variantProps[viewportKey] if (!viewportValue) continue const breakpoint = viewportToBreakpoint(viewportValue) - // Create group key from non-viewport variants + // Create group key from non-viewport, non-effect variants const otherVariants = Object.entries(variantProps) - .filter(([key]) => key.toLowerCase() !== 'viewport') + .filter(([key]) => { + const lowerKey = key.toLowerCase() + return lowerKey !== 'viewport' && lowerKey !== 'effect' + }) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}=${value}`) .join('|') @@ -316,11 +327,6 @@ export class ResponsiveCodegen { } } - // Check if componentSet has effect variant (pseudo-selector) - const hasEffect = Object.keys( - componentSet.componentPropertyDefinitions, - ).some((key) => key.toLowerCase() === 'effect') - // Generate responsive code for each group const results: Array = [] const responsiveCodegen = new ResponsiveCodegen(null) @@ -338,20 +344,26 @@ export class ResponsiveCodegen { } } - // Get pseudo-selector props for this specific variant group - const selectorProps = hasEffect - ? await getSelectorPropsForGroup(componentSet, variantFilter) - : null - // Build trees for each viewport const treesByBreakpoint = new Map() for (const [bp, component] of viewportComponents) { const codegen = new Codegen(component) const tree = await codegen.getTree() - // Add pseudo-selector props to tree - if (selectorProps && Object.keys(selectorProps).length > 0) { - Object.assign(tree.props, selectorProps) + + // Get pseudo-selector props for this specific variant group AND viewport + // This ensures hover/active colors are correctly responsive per viewport + if (effectKey) { + const viewportValue = component.variantProperties?.[viewportKey] + const selectorProps = await getSelectorPropsForGroup( + componentSet, + variantFilter, + viewportValue, + ) + if (Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } } + treesByBreakpoint.set(bp, tree) } @@ -502,20 +514,27 @@ export class ResponsiveCodegen { >() for (const [compositeKey, viewportComponents] of byCompositeVariant) { - // Get pseudo-selector props for this specific variant group const variantFilter = parseCompositeKey(compositeKey) - const selectorProps = effectKey - ? await getSelectorPropsForGroup(componentSet, variantFilter) - : null const treesByBreakpoint = new Map() for (const [bp, component] of viewportComponents) { const codegen = new Codegen(component) const tree = await codegen.getTree() - // Add pseudo-selector props to tree - if (selectorProps && Object.keys(selectorProps).length > 0) { - Object.assign(tree.props, selectorProps) + + // Get pseudo-selector props for this specific variant group AND viewport + // This ensures hover/active colors are correctly responsive per viewport + if (effectKey) { + const viewportValue = component.variantProperties?.[viewportKey] + const selectorProps = await getSelectorPropsForGroup( + componentSet, + variantFilter, + viewportValue, + ) + if (Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } } + treesByBreakpoint.set(bp, tree) } responsivePropsByComposite.set(compositeKey, treesByBreakpoint) @@ -1107,6 +1126,7 @@ export class ResponsiveCodegen { /** * Create a nested variant prop value for props that differ across multiple variant dimensions. + * Optimized to minimize nesting depth by choosing the best outer variant key. */ private createNestedVariantProp( variantKeys: string[], @@ -1149,10 +1169,16 @@ export class ResponsiveCodegen { ) } - // For multiple variant keys, we need to determine which key(s) cause the difference - // and create nested conditionals + // For multiple variant keys, calculate nesting cost for each possible outer key + // and choose the one with minimum cost + interface CandidateResult { + variantKey: string + cost: number + nestedValues: Record + } + + const candidates: CandidateResult[] = [] - // Try each variant key to see if it alone explains the difference for (const variantKey of variantKeys) { const valuesByVariant = new Map>() @@ -1174,68 +1200,76 @@ export class ResponsiveCodegen { } } - // Check if this variant key alone explains the difference - // (all values within each variant value are the same) - let variantExplainsAll = true - const simplifiedValues: Record = {} + // Calculate nested values and cost for this outer key + const remainingKeys = variantKeys.filter((k) => k !== variantKey) + const nestedValues: Record = {} + let totalCost = 0 for (const [variantValue, subValues] of valuesByVariant) { + // Check if all sub-values are the same (can collapse to scalar) const uniqueSubValues = new Set( [...subValues.values()].map((v) => JSON.stringify(v)), ) + if (uniqueSubValues.size === 1) { - simplifiedValues[variantValue] = [...subValues.values()][0] + // All same - collapse to scalar value (cost 0) + nestedValues[variantValue] = [...subValues.values()][0] + // Cost is 0 for scalar } else { - variantExplainsAll = false - break + // Need to recurse + const nestedResult = this.createNestedVariantProp( + remainingKeys, + subValues, + ) + nestedValues[variantValue] = nestedResult + + // Calculate cost based on nesting depth + totalCost += this.calculateNestingCost(nestedResult) } } - if (variantExplainsAll) { - // This variant key alone explains the difference - return createVariantPropValue( - variantKey, - simplifiedValues as Record, - ) - } + candidates.push({ + variantKey, + cost: totalCost, + nestedValues, + }) } - // Multiple variant keys contribute to the difference - // Use the first variant key and recurse for nested conditionals - const primaryKey = variantKeys[0] - const remainingKeys = variantKeys.slice(1) - - const valuesByPrimaryVariant = new Map>() - - for (const [compositeKey, value] of valuesByComposite) { - const parsed = parseCompositeKey(compositeKey) - const primaryValue = parsed[primaryKey] - - if (!valuesByPrimaryVariant.has(primaryValue)) { - valuesByPrimaryVariant.set(primaryValue, new Map()) - } - - const subCompositeKey = remainingKeys - .map((k) => `${k}=${parsed[k]}`) - .join('|') - const primaryMap = valuesByPrimaryVariant.get(primaryValue) - if (primaryMap) { - primaryMap.set(subCompositeKey, value) + // Find the candidate with minimum cost + let bestCandidate = candidates[0] + for (const candidate of candidates) { + if (candidate.cost < bestCandidate.cost) { + bestCandidate = candidate } } - // Create nested structure - const nestedValues: Record = {} - for (const [primaryValue, subValues] of valuesByPrimaryVariant) { - nestedValues[primaryValue] = this.createNestedVariantProp( - remainingKeys, - subValues, - ) - } - return createVariantPropValue( - primaryKey, - nestedValues as Record, + bestCandidate.variantKey, + bestCandidate.nestedValues as Record, ) } + + /** + * Calculate the nesting cost of a value. + * Scalar values have cost 0, VariantPropValue adds 1 + cost of nested values. + */ + private calculateNestingCost(value: unknown): number { + if ( + typeof value === 'object' && + value !== null && + '__variantProp' in value + ) { + const variantProp = value as { + __variantProp: true + values: Record + } + let maxNestedCost = 0 + for (const nestedValue of Object.values(variantProp.values)) { + const nestedCost = this.calculateNestingCost(nestedValue) + maxNestedCost = Math.max(maxNestedCost, nestedCost) + } + return 1 + maxNestedCost + } + return 0 + } } diff --git a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts index 35ec8e3..87e043a 100644 --- a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts +++ b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts @@ -572,6 +572,146 @@ describe('ResponsiveCodegen', () => { }) }) + describe('createNestedVariantProp optimization', () => { + it('minimizes nesting by choosing the best outer variant key', () => { + const generator = new ResponsiveCodegen(null) + + // Scenario: bg prop where white is same for both sizes, but primary differs + // If we use size as outer key: + // { Md: { primary: "$primaryBold", white: "$background" }[variant], + // Sm: { primary: "$primaryExBold", white: "$background" }[variant] }[size] + // If we use variant as outer key: + // { primary: { Md: "$primaryBold", Sm: "$primaryExBold" }[size], + // white: "$background" }[variant] + // The second option is better because white collapses to a scalar + + const treesByVariant = new Map([ + [ + 'size=Md|variant=primary', + { + component: 'Box', + props: { bg: '$primaryBold' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Md|variant=white', + { + component: 'Box', + props: { bg: '$background' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Sm|variant=primary', + { + component: 'Box', + props: { bg: '$primaryExBold' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Sm|variant=white', + { + component: 'Box', + props: { bg: '$background' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]) + + // Use generateNestedVariantMergedCode which internally calls createNestedVariantProp + const result = ( + generator as unknown as { + generateNestedVariantMergedCode: ( + variantKeys: string[], + trees: Map, + depth: number, + ) => string + } + ).generateNestedVariantMergedCode(['size', 'variant'], treesByVariant, 0) + + // Should use variant as outer key because white collapses to scalar + // The result should have variant as the outer conditional, not size + expect(result).toContain('variantKey":"variant"') + // white should be a scalar value, not nested + expect(result).toContain('"white":"$background"') + // primary should be nested with size + expect(result).toContain('"primary":{') + expect(result).toContain('variantKey":"size"') + }) + + it('uses any key when all have equal nesting cost', () => { + const generator = new ResponsiveCodegen(null) + + // Scenario: all combinations have different values + const treesByVariant = new Map([ + [ + 'size=Md|variant=primary', + { + component: 'Box', + props: { bg: 'A' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Md|variant=white', + { + component: 'Box', + props: { bg: 'B' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Sm|variant=primary', + { + component: 'Box', + props: { bg: 'C' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'size=Sm|variant=white', + { + component: 'Box', + props: { bg: 'D' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]) + + const result = ( + generator as unknown as { + generateNestedVariantMergedCode: ( + variantKeys: string[], + trees: Map, + depth: number, + ) => string + } + ).generateNestedVariantMergedCode(['size', 'variant'], treesByVariant, 0) + + // Should still produce valid output with either key as outer + expect(result).toContain('variantKey') + expect(result).toContain('"bg"') + }) + }) + describe('generateVariantResponsiveComponents', () => { it('handles component set with only non-viewport variants', async () => { const componentSet = { @@ -862,5 +1002,84 @@ describe('ResponsiveCodegen', () => { // Verify snapshot for the generated code expect(code).toMatchSnapshot() }) + + it('handles responsive pseudo-selector props (different hover colors per viewport)', async () => { + // Component with different hover colors for Desktop vs Mobile + const createComponent = ( + effect: string, + viewport: string, + bgColor: { r: number; g: number; b: number }, + ) => + ({ + type: 'COMPONENT', + name: `effect=${effect}, viewport=${viewport}`, + variantProperties: { effect, viewport }, + children: [], + layoutMode: 'HORIZONTAL', + width: viewport === 'Desktop' ? 200 : 150, + height: 50, + fills: [ + { + type: 'SOLID', + visible: true, + color: bgColor, + opacity: 1, + }, + ], + reactions: [], + }) as unknown as ComponentNode + + // Desktop colors + const desktopDefault = { r: 0.5, g: 0.5, b: 0.5 } // #808080 + const desktopHover = { r: 0.6, g: 0.6, b: 0.6 } // #999999 + + // Mobile colors (different hover color) + const mobileDefault = { r: 0.5, g: 0.5, b: 0.5 } // #808080 (same as desktop) + const mobileHover = { r: 0.7, g: 0.7, b: 0.7 } // #B3B3B3 (different from desktop) + + const componentSet = { + type: 'COMPONENT_SET', + name: 'ResponsiveHoverButton', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover'], + }, + viewport: { + type: 'VARIANT', + defaultValue: 'Desktop', + variantOptions: ['Desktop', 'Mobile'], + }, + }, + children: [ + createComponent('default', 'Desktop', desktopDefault), + createComponent('hover', 'Desktop', desktopHover), + createComponent('default', 'Mobile', mobileDefault), + createComponent('hover', 'Mobile', mobileHover), + ], + } as unknown as ComponentSetNode + + // Set default variant + ;(componentSet as { defaultVariant: ComponentNode }).defaultVariant = + componentSet.children[0] as ComponentNode + + const result = + await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'ResponsiveHoverButton', + ) + + expect(result.length).toBe(1) + const code = result[0][1] + + // The _hover.bg should be a responsive array since colors differ by viewport + // Desktop hover: #999 (or #999999), Mobile hover: #B3B3B3 + // Expected: _hover: { bg: ["#B3B3B3", null, null, null, "#999"] } + expect(code).toContain('_hover') + // Should contain responsive array format (mobile first, then pc) + expect(code).toContain('#B3B3B3') // Mobile hover color + expect(code).toMatch(/#999(?:999)?/) // Desktop hover color (may be shortened) + }) }) }) From d14a70ca3500b3a074adbec70f3df307d60daaf4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 17:45:02 +0900 Subject: [PATCH 37/51] Selector with responsive --- .../__tests__/ResponsiveCodegen.test.ts | 77 +++++++++++++++++++ src/codegen/responsive/index.ts | 10 ++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts index 87e043a..648ba03 100644 --- a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts +++ b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts @@ -1081,5 +1081,82 @@ describe('ResponsiveCodegen', () => { expect(code).toContain('#B3B3B3') // Mobile hover color expect(code).toMatch(/#999(?:999)?/) // Desktop hover color (may be shortened) }) + + it('handles hover only on desktop (no hover on mobile)', async () => { + // Component where hover only exists on Desktop, not on Mobile + const createComponent = ( + effect: string, + viewport: string, + bgColor: { r: number; g: number; b: number }, + ) => + ({ + type: 'COMPONENT', + name: `effect=${effect}, viewport=${viewport}`, + variantProperties: { effect, viewport }, + children: [], + layoutMode: 'HORIZONTAL', + width: viewport === 'Desktop' ? 200 : 150, + height: 50, + fills: [ + { + type: 'SOLID', + visible: true, + color: bgColor, + opacity: 1, + }, + ], + reactions: [], + }) as unknown as ComponentNode + + // Desktop colors + const desktopDefault = { r: 0.5, g: 0.5, b: 0.5 } // #808080 + const desktopHover = { r: 0.6, g: 0.6, b: 0.6 } // #999999 + + // Mobile - only default, NO hover variant + const mobileDefault = { r: 0.5, g: 0.5, b: 0.5 } // #808080 + + const componentSet = { + type: 'COMPONENT_SET', + name: 'DesktopOnlyHoverButton', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover'], + }, + viewport: { + type: 'VARIANT', + defaultValue: 'Desktop', + variantOptions: ['Desktop', 'Mobile'], + }, + }, + children: [ + createComponent('default', 'Desktop', desktopDefault), + createComponent('hover', 'Desktop', desktopHover), + createComponent('default', 'Mobile', mobileDefault), + // Note: NO hover variant for Mobile + ], + } as unknown as ComponentSetNode + + // Set default variant + ;(componentSet as { defaultVariant: ComponentNode }).defaultVariant = + componentSet.children[0] as ComponentNode + + const result = + await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'DesktopOnlyHoverButton', + ) + + expect(result.length).toBe(1) + const code = result[0][1] + + // _hover should exist with responsive array where mobile slot is null + // Expected: _hover: { bg: [null, null, null, null, "#999"] } + expect(code).toContain('_hover') + // Should contain null for mobile slot and value for pc slot + expect(code).toContain('null') + expect(code).toMatch(/#999(?:999)?/) // Desktop hover color + }) }) }) diff --git a/src/codegen/responsive/index.ts b/src/codegen/responsive/index.ts index b731392..97fe3d5 100644 --- a/src/codegen/responsive/index.ts +++ b/src/codegen/responsive/index.ts @@ -231,7 +231,10 @@ export function mergePropsToResponsive( // Their inner props should be merged into responsive arrays if (isPseudoSelectorProp(key)) { // Collect pseudo-selector objects from each breakpoint + // For breakpoints that don't have this pseudo-selector, use empty object + // so that inner props get null values for those breakpoints const pseudoPropsMap = new Map>() + let hasPseudoSelector = false for (const [bp, props] of breakpointProps) { if ( key in props && @@ -239,9 +242,14 @@ export function mergePropsToResponsive( props[key] !== null ) { pseudoPropsMap.set(bp, props[key] as Record) + hasPseudoSelector = true + } else { + // Breakpoint doesn't have this pseudo-selector, use empty object + // This ensures inner props get null for this breakpoint + pseudoPropsMap.set(bp, {}) } } - if (pseudoPropsMap.size > 0) { + if (hasPseudoSelector) { // Recursively merge the inner props of pseudo-selector result[key] = mergePropsToResponsive(pseudoPropsMap) } From 599e287a289f507257b976f6d289bf6487cebdfd Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 18:35:13 +0900 Subject: [PATCH 38/51] Selector with responsive --- src/codegen/responsive/ResponsiveCodegen.ts | 56 ++++++-- .../__tests__/ResponsiveCodegen.test.ts | 127 ++++++++++++++++++ src/codegen/responsive/index.ts | 10 +- 3 files changed, 180 insertions(+), 13 deletions(-) diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 5056289..93ce743 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -1072,7 +1072,10 @@ export class ResponsiveCodegen { // Check if prop is a pseudo-selector (needs special handling) if (propKey.startsWith('_')) { // Collect pseudo-selector props across all composites + // For composites that don't have this pseudo-selector, use empty object + // so that inner props get null values for those composites const pseudoPropsMap = new Map>() + let hasPseudoSelector = false for (const [compositeKey, props] of propsMap) { if ( propKey in props && @@ -1083,9 +1086,14 @@ export class ResponsiveCodegen { compositeKey, props[propKey] as Record, ) + hasPseudoSelector = true + } else { + // Composite doesn't have this pseudo-selector, use empty object + // This ensures inner props get null for this composite + pseudoPropsMap.set(compositeKey, {}) } } - if (pseudoPropsMap.size > 0) { + if (hasPseudoSelector) { result[propKey] = this.mergePropsAcrossComposites( variantKeys, pseudoPropsMap, @@ -1095,29 +1103,48 @@ export class ResponsiveCodegen { } // Collect values for this prop across all composites + // For composites that don't have this prop, use null const valuesByComposite = new Map() + let hasValue = false for (const [compositeKey, props] of propsMap) { if (propKey in props) { valuesByComposite.set(compositeKey, props[propKey]) + hasValue = true + } else { + // Composite doesn't have this prop, use null + valuesByComposite.set(compositeKey, null) } } - // Check if all values are the same + if (!hasValue) continue + + // Check if all values are the same (including null checks) const uniqueValues = new Set() for (const value of valuesByComposite.values()) { uniqueValues.add(JSON.stringify(value)) } - if (uniqueValues.size === 1) { - // All values are the same - use as-is - result[propKey] = [...valuesByComposite.values()][0] + const firstValue = [...valuesByComposite.values()][0] + if (uniqueValues.size === 1 && firstValue !== null) { + // All values are the same and not null - use as-is + result[propKey] = firstValue } else { - // Values differ - need to create variant conditional - // Try to find which variant key causes the difference - result[propKey] = this.createNestedVariantProp( - variantKeys, - valuesByComposite, - ) + // Values differ or some are null - need to create variant conditional + // Filter out null values for the conditional (only include composites that have the prop) + const nonNullValues = new Map() + for (const [compositeKey, value] of valuesByComposite) { + if (value !== null) { + nonNullValues.set(compositeKey, value) + } + } + + if (nonNullValues.size > 0) { + // Try to find which variant key causes the difference + result[propKey] = this.createNestedVariantProp( + variantKeys, + nonNullValues, + ) + } } } @@ -1228,9 +1255,14 @@ export class ResponsiveCodegen { } } + // Add the number of entries as a secondary cost factor + // This ensures we prefer fewer entries when nesting costs are equal + // Multiply by 0.1 to make it a tiebreaker (less important than nesting depth) + const entryCost = Object.keys(nestedValues).length * 0.1 + candidates.push({ variantKey, - cost: totalCost, + cost: totalCost + entryCost, nestedValues, }) } diff --git a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts index 648ba03..adb6882 100644 --- a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts +++ b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts @@ -1158,5 +1158,132 @@ describe('ResponsiveCodegen', () => { expect(code).toContain('null') expect(code).toMatch(/#999(?:999)?/) // Desktop hover color }) + + it('handles hover prop existing only in some variants (outline only in white)', async () => { + // Component where _hover.outline exists only in white variant, not in primary + const createComponent = ( + effect: string, + variant: string, + bgColor: { r: number; g: number; b: number }, + strokes?: Array<{ + type: string + visible: boolean + color: { r: number; g: number; b: number } + }>, + ) => + ({ + type: 'COMPONENT', + name: `effect=${effect}, variant=${variant}`, + variantProperties: { effect, variant }, + children: [], + layoutMode: 'HORIZONTAL', + width: 200, + height: 50, + fills: [ + { + type: 'SOLID', + visible: true, + color: bgColor, + opacity: 1, + }, + ], + strokes: strokes || [], + strokeWeight: strokes && strokes.length > 0 ? 1 : 0, + reactions: [], + }) as unknown as ComponentNode + + // primary variant - no border/stroke + const primaryDefault = { r: 0.5, g: 0.2, b: 0.1 } + const primaryHover = { r: 0.6, g: 0.3, b: 0.2 } + + // white variant - has border/stroke on hover + const whiteDefault = { r: 1, g: 1, b: 1 } + const whiteHover = { r: 0.95, g: 0.95, b: 0.95 } + const whiteBorderHover = [ + { type: 'SOLID', visible: true, color: { r: 0, g: 0, b: 0 } }, + ] + + const componentSet = { + type: 'COMPONENT_SET', + name: 'BorderVariantButton', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover'], + }, + variant: { + type: 'VARIANT', + defaultValue: 'primary', + variantOptions: ['primary', 'white'], + }, + }, + children: [ + createComponent('default', 'primary', primaryDefault), + createComponent('hover', 'primary', primaryHover), + createComponent('default', 'white', whiteDefault), + createComponent('hover', 'white', whiteHover, whiteBorderHover), + ], + } as unknown as ComponentSetNode + + // Set default variant + ;(componentSet as { defaultVariant: ComponentNode }).defaultVariant = + componentSet.children[0] as ComponentNode + + const result = + await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'BorderVariantButton', + ) + + expect(result.length).toBe(1) + const code = result[0][1] + + // _hover should exist with outline that only applies to white variant + // Since primary doesn't have outline in hover, the variant conditional should + // only include white: { outline: "solid 1px #000" }[variant] + expect(code).toContain('_hover') + // Should contain the outline value for white (strokes become outline) + expect(code).toContain('outline') + // The outline should be wrapped in variant conditional with only white having value + expect(code).toContain('variantKey') + expect(code).toContain('"white"') + // primary should NOT have outline value (it has no border/stroke) + expect(code).toMatch(/"outline":\{[^}]*"white":[^}]*\}/) + expect(code).not.toMatch(/"outline":\{[^}]*"primary":/) + }) + + it('prefers variant key with fewer entries via createNestedVariantProp', () => { + // When a prop exists only in white variant (for both Md and Sm sizes), + // using 'variant' as outer key produces 1 entry: { white: "..." }[variant] + // using 'size' as outer key produces 2 entries: { Md: "...", Sm: "..." }[size] + // Should prefer 'variant' for fewer entries + const generator = new ResponsiveCodegen(null) + + // Scenario: border only exists for white variant (Md and Sm both have it) + // white Md and white Sm have the same border value + const valuesByComposite = new Map([ + ['size=Md|variant=white', 'solid 1px #000'], + ['size=Sm|variant=white', 'solid 1px #000'], + ]) + + // Access private method via type casting + const result = ( + generator as unknown as { + createNestedVariantProp: ( + variantKeys: string[], + valuesByComposite: Map, + ) => unknown + } + ).createNestedVariantProp(['size', 'variant'], valuesByComposite) + + // Result should use 'variant' as key since it produces fewer entries (1 entry: white) + // NOT 'size' (which would produce 2 entries: Md, Sm with same value) + expect(result).toEqual({ + __variantProp: true, + variantKey: 'variant', + values: { white: 'solid 1px #000' }, + }) + }) }) }) diff --git a/src/codegen/responsive/index.ts b/src/codegen/responsive/index.ts index 97fe3d5..fb45af9 100644 --- a/src/codegen/responsive/index.ts +++ b/src/codegen/responsive/index.ts @@ -430,7 +430,10 @@ export function mergePropsToVariant( // Their inner props should be merged with variant conditionals if (isPseudoSelectorProp(key)) { // Collect pseudo-selector objects from each variant + // For variants that don't have this pseudo-selector, use empty object + // so that inner props get null values for those variants const pseudoPropsMap = new Map>() + let hasPseudoSelector = false for (const [variant, props] of variantProps) { if ( key in props && @@ -438,9 +441,14 @@ export function mergePropsToVariant( props[key] !== null ) { pseudoPropsMap.set(variant, props[key] as Record) + hasPseudoSelector = true + } else { + // Variant doesn't have this pseudo-selector, use empty object + // This ensures inner props get null for this variant + pseudoPropsMap.set(variant, {}) } } - if (pseudoPropsMap.size > 0) { + if (hasPseudoSelector) { // Recursively merge the inner props of pseudo-selector result[key] = mergePropsToVariant(variantKey, pseudoPropsMap) } From b888ffa84721d78d5ee339c9a7b228541c89d1ef Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 18:53:08 +0900 Subject: [PATCH 39/51] Selector with responsive --- .../utils/__tests__/props-to-str.test.ts | 19 ++++-- src/codegen/utils/props-to-str.ts | 63 +++++++++++++++++-- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/codegen/utils/__tests__/props-to-str.test.ts b/src/codegen/utils/__tests__/props-to-str.test.ts index 4950632..f5a5bda 100644 --- a/src/codegen/utils/__tests__/props-to-str.test.ts +++ b/src/codegen/utils/__tests__/props-to-str.test.ts @@ -89,9 +89,10 @@ describe('propsToString', () => { default: ['30px', null, '40px'], }) const res = propsToString({ w: variantProp }) - expect(res).toBe( - 'w={{ scroll: ["10px", null, "20px"], default: ["30px", null, "40px"] }[status]}', - ) + // Array values trigger multiline format + expect(res).toContain('scroll: ["10px", null, "20px"]') + expect(res).toContain('default: ["30px", null, "40px"]') + expect(res).toContain('[status]') }) test('handles VariantPropValue with numeric values', () => { @@ -123,8 +124,14 @@ describe('propsToString', () => { default: { x: 3, y: 4 }, }) const res = propsToString({ transform: variantProp }) - expect(res).toContain('scroll: {"x":1,"y":2}') - expect(res).toContain('default: {"x":3,"y":4}') + // Object values trigger multiline format with proper indentation + expect(res).toContain('scroll:') + expect(res).toContain('"x": 1') + expect(res).toContain('"y": 2') + expect(res).toContain('default:') + expect(res).toContain('"x": 3') + expect(res).toContain('"y": 4') + expect(res).toContain('[status]') }) test('handles VariantPropValue with boolean values', () => { @@ -142,8 +149,10 @@ describe('propsToString', () => { default: ['20px', undefined], }) const res = propsToString({ w: variantProp }) + // Array values trigger multiline format expect(res).toContain('scroll: [undefined, "10px"]') expect(res).toContain('default: ["20px", undefined]') + expect(res).toContain('[status]') }) test('handles VariantPropValue with symbol values (fallback case)', () => { diff --git a/src/codegen/utils/props-to-str.ts b/src/codegen/utils/props-to-str.ts index 85d34ca..55c7889 100644 --- a/src/codegen/utils/props-to-str.ts +++ b/src/codegen/utils/props-to-str.ts @@ -25,20 +25,73 @@ function valueToJsxString(value: unknown): string { return String(value) } +/** + * Check if a VariantPropValue needs multiline formatting. + * Returns true if any value is complex (array, object, or nested VariantPropValue). + */ +function needsMultilineFormat(values: Record): boolean { + return Object.values(values).some( + (value) => + Array.isArray(value) || + (typeof value === 'object' && value !== null) || + isVariantPropValue(value), + ) +} + /** * Format a VariantPropValue as JSX: { scroll: [...], default: [...] }[status] + * Uses multiline format when values are complex (arrays, objects, nested variants). */ -function formatVariantPropValue(variantProp: { - variantKey: string - values: Record -}): string { +function formatVariantPropValue( + variantProp: { + variantKey: string + values: Record + }, + indent: number = 0, +): string { const entries = Object.entries(variantProp.values) + + // Use multiline format for complex values + if (needsMultilineFormat(variantProp.values)) { + const spaces = ' '.repeat(indent + 1) + const closingSpaces = ' '.repeat(indent) + const parts = entries.map(([variant, value]) => { + const formattedValue = formatValueWithIndent(value, indent + 1) + return `${spaces}${variant}: ${formattedValue}` + }) + return `{\n${parts.join(',\n')}\n${closingSpaces}}[${variantProp.variantKey}]` + } + + // Simple inline format for primitive values const parts = entries.map(([variant, value]) => { return `${variant}: ${valueToJsxString(value)}` }) return `{ ${parts.join(', ')} }[${variantProp.variantKey}]` } +/** + * Format a value with proper indentation for multiline output. + */ +function formatValueWithIndent(value: unknown, indent: number): string { + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (typeof value === 'string') return `"${value}"` + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + if (Array.isArray(value)) { + const items = value.map((item) => valueToJsxString(item)) + return `[${items.join(', ')}]` + } + if (isVariantPropValue(value)) { + return formatVariantPropValue(value, indent) + } + if (typeof value === 'object') { + return objectToJsxString(value as Record, indent) + } + return String(value) +} + /** * Convert an object to JSX string, handling nested VariantPropValue. * Uses JSON.stringify-like formatting but replaces VariantPropValue with proper syntax. @@ -74,7 +127,7 @@ function formatObjectValue(value: unknown, indent: number): string { return `[${items.join(', ')}]` } if (isVariantPropValue(value)) { - return formatVariantPropValue(value) + return formatVariantPropValue(value, indent) } if (typeof value === 'object') { return objectToJsxString(value as Record, indent) From 4256214f6c2cccd20d7787eabee466d8e7db5b5b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 19:05:53 +0900 Subject: [PATCH 40/51] Selector with responsive --- .../utils/__tests__/props-to-str.test.ts | 24 ++++++++++++++----- src/codegen/utils/props-to-str.ts | 8 ++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/codegen/utils/__tests__/props-to-str.test.ts b/src/codegen/utils/__tests__/props-to-str.test.ts index f5a5bda..0b6e724 100644 --- a/src/codegen/utils/__tests__/props-to-str.test.ts +++ b/src/codegen/utils/__tests__/props-to-str.test.ts @@ -80,7 +80,10 @@ describe('propsToString', () => { default: '20px', }) const res = propsToString({ w: variantProp }) - expect(res).toBe('w={{ scroll: "10px", default: "20px" }[status]}') + // 2+ entries always use multiline format + expect(res).toContain('scroll: "10px"') + expect(res).toContain('default: "20px"') + expect(res).toContain('[status]') }) test('handles VariantPropValue with array values (responsive)', () => { @@ -101,10 +104,13 @@ describe('propsToString', () => { lg: 20, }) const res = propsToString({ gap: variantProp }) - expect(res).toBe('gap={{ sm: 10, lg: 20 }[size]}') + // 2+ entries always use multiline format + expect(res).toContain('sm: 10') + expect(res).toContain('lg: 20') + expect(res).toContain('[size]') }) - test('VariantPropValue does not trigger newline separator', () => { + test('VariantPropValue does not trigger newline separator between props', () => { const variantProp = createVariantPropValue('status', { scroll: '10px', default: '20px', @@ -114,8 +120,11 @@ describe('propsToString', () => { h: '100px', bg: 'red', }) - // Should use space separator, not newline - expect(res).not.toContain('\n') + // Props should be separated by space (not newline between props) + // But the VariantPropValue itself uses multiline format internally + expect(res).toContain('bg="red"') + expect(res).toContain('h="100px"') + expect(res).toContain('[status]') }) test('handles VariantPropValue with object values', () => { @@ -140,7 +149,10 @@ describe('propsToString', () => { default: false, }) const res = propsToString({ visible: variantProp }) - expect(res).toBe('visible={{ scroll: true, default: false }[status]}') + // 2+ entries always use multiline format + expect(res).toContain('scroll: true') + expect(res).toContain('default: false') + expect(res).toContain('[status]') }) test('handles VariantPropValue with undefined values in array', () => { diff --git a/src/codegen/utils/props-to-str.ts b/src/codegen/utils/props-to-str.ts index 55c7889..e1f9978 100644 --- a/src/codegen/utils/props-to-str.ts +++ b/src/codegen/utils/props-to-str.ts @@ -27,9 +27,15 @@ function valueToJsxString(value: unknown): string { /** * Check if a VariantPropValue needs multiline formatting. - * Returns true if any value is complex (array, object, or nested VariantPropValue). + * Returns true if: + * - There are 2 or more variant entries, OR + * - Any value is complex (array, object, or nested VariantPropValue) */ function needsMultilineFormat(values: Record): boolean { + const entries = Object.entries(values) + // Always use multiline if there are 2+ entries + if (entries.length >= 2) return true + // Also use multiline for complex values return Object.values(values).some( (value) => Array.isArray(value) || From 21a2cd3f70b461ed0a5d469cf46af38e2db3c6b3 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 19:15:04 +0900 Subject: [PATCH 41/51] Update selector --- src/codegen/props/selector.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index 2e9946d..07353ff 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -1,6 +1,35 @@ import { fmtPct } from '../utils/fmtPct' import { getProps } from '.' +// Shorthand prop names to CSS standard property names +const shortToCssProperty: Record = { + bg: 'background', + w: 'width', + h: 'height', + p: 'padding', + pt: 'padding-top', + pr: 'padding-right', + pb: 'padding-bottom', + pl: 'padding-left', + px: 'padding-inline', + py: 'padding-block', + m: 'margin', + mt: 'margin-top', + mr: 'margin-right', + mb: 'margin-bottom', + ml: 'margin-left', + mx: 'margin-inline', + my: 'margin-block', + pos: 'position', +} + +/** + * Convert shorthand prop names to CSS standard property names for transitionProperty. + */ +function toTransitionPropertyName(key: string): string { + return shortToCssProperty[key] || key +} + // 속성 이름을 유효한 TypeScript 식별자로 변환 const toUpperCase = (_: string, chr: string) => chr.toUpperCase() @@ -93,7 +122,7 @@ export async function getSelectorProps( } } if (transition?.type === 'SMART_ANIMATE' && diffKeys.size > 0) { - const keys = Array.from(diffKeys) + const keys = Array.from(diffKeys).map(toTransitionPropertyName) keys.sort() result.props.transition = `${fmtPct(transition.duration)}ms ${transition.easing.type.toLocaleLowerCase().replaceAll('_', '-')}` result.props.transitionProperty = keys.join(',') @@ -178,7 +207,7 @@ export async function getSelectorPropsForGroup( ?.flatMap(getTransition) .flat()[0] if (transition?.type === 'SMART_ANIMATE') { - const keys = Array.from(diffKeys) + const keys = Array.from(diffKeys).map(toTransitionPropertyName) keys.sort() result.transition = `${fmtPct(transition.duration)}ms ${transition.easing.type.toLocaleLowerCase().replaceAll('_', '-')}` result.transitionProperty = keys.join(',') From 36effccd7195429cb911b5bd0ba033b9157d8c90 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 19:54:12 +0900 Subject: [PATCH 42/51] Update selector --- src/codegen/props/__tests__/selector.test.ts | 158 +++++++++++++++++++ src/codegen/responsive/ResponsiveCodegen.ts | 41 ++++- 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/src/codegen/props/__tests__/selector.test.ts b/src/codegen/props/__tests__/selector.test.ts index 8455785..a875c94 100644 --- a/src/codegen/props/__tests__/selector.test.ts +++ b/src/codegen/props/__tests__/selector.test.ts @@ -332,6 +332,164 @@ describe('getSelectorProps', () => { expect(result?.props._active).toBeDefined() }) + test('handles effect-only COMPONENT_SET with solid button', async () => { + // This tests a button component with only effect variant + // effect options: active, default, disabled, hover + // Using SOLID fills to avoid gradient transform complexity in tests + const defaultVariant = { + type: 'COMPONENT', + name: 'effect=default', + children: [], + visible: true, + variantProperties: { effect: 'default' }, + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.5, g: 0.3, b: 0.9 }, + opacity: 1, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + color: { r: 0, g: 0, b: 0, a: 0.1 }, + offset: { x: 0, y: 2 }, + spread: -2, + }, + ], + opacity: 1, + reactions: [ + { + trigger: { type: 'ON_HOVER' }, + actions: [ + { + type: 'NODE', + transition: { + type: 'SMART_ANIMATE', + duration: 0.3, + easing: { type: 'EASE_OUT' }, + }, + }, + ], + }, + ], + } as unknown as ComponentNode + + const hoverVariant = { + type: 'COMPONENT', + name: 'effect=hover', + children: [], + visible: true, + variantProperties: { effect: 'hover' }, + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.4, g: 0.2, b: 0.8 }, + opacity: 1, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 6, + color: { r: 0, g: 0, b: 0, a: 0.1 }, + offset: { x: 0, y: 4 }, + spread: -4, + }, + ], + opacity: 1, + reactions: [], + } as unknown as ComponentNode + + const activeVariant = { + type: 'COMPONENT', + name: 'effect=active', + children: [], + visible: true, + variantProperties: { effect: 'active' }, + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.4, g: 0.2, b: 0.8 }, + opacity: 1, + }, + ], + effects: [], + opacity: 0.8, + reactions: [], + } as unknown as ComponentNode + + const disabledVariant = { + type: 'COMPONENT', + name: 'effect=disabled', + children: [], + visible: true, + variantProperties: { effect: 'disabled' }, + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.898, g: 0.906, b: 0.922 }, + opacity: 1, + }, + ], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + color: { r: 0, g: 0, b: 0, a: 0.1 }, + offset: { x: 0, y: 2 }, + spread: -2, + }, + ], + opacity: 1, + reactions: [], + } as unknown as ComponentNode + + const node = { + type: 'COMPONENT_SET', + name: 'SolidButton', + children: [ + hoverVariant, + activeVariant, + defaultVariant, + disabledVariant, + ], + defaultVariant, + visible: true, + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['active', 'default', 'disabled', 'hover'], + }, + }, + } as unknown as ComponentSetNode + + const result = await getSelectorProps(node) + + // Should return result even with effect-only component set + expect(result).toBeDefined() + // variants should be empty (effect is not included in variants) + expect(Object.keys(result?.variants || {})).toHaveLength(0) + // Should have _hover props + expect(result?.props._hover).toBeDefined() + // Should have _active props + expect(result?.props._active).toBeDefined() + // Should have _disabled props + expect(result?.props._disabled).toBeDefined() + // Should have transition props (from SMART_ANIMATE) + expect(result?.props.transition).toBeDefined() + expect(result?.props.transitionProperty).toBeDefined() + }) + test('handles SMART_ANIMATE transition with reactions', async () => { const defaultVariant = { type: 'COMPONENT', diff --git a/src/codegen/responsive/ResponsiveCodegen.ts b/src/codegen/responsive/ResponsiveCodegen.ts index 93ce743..ec49ce3 100644 --- a/src/codegen/responsive/ResponsiveCodegen.ts +++ b/src/codegen/responsive/ResponsiveCodegen.ts @@ -424,9 +424,12 @@ export class ResponsiveCodegen { } } - // If effect variant only, skip component rendering (effect is pseudo-selector) + // If effect variant only, generate code from defaultVariant with pseudo-selectors if (effectKey && !viewportKey && otherVariantKeys.length === 0) { - return [] + return ResponsiveCodegen.generateEffectOnlyComponents( + componentSet, + componentName, + ) } // If no viewport variant, just handle other variants @@ -553,6 +556,40 @@ export class ResponsiveCodegen { return result } + /** + * Generate component code for COMPONENT_SET with effect variant only (no other variants). + * Uses defaultVariant as the base and adds pseudo-selector props from getSelectorProps. + */ + private static async generateEffectOnlyComponents( + componentSet: ComponentSetNode, + componentName: string, + ): Promise> { + // Use defaultVariant as the base component + const defaultComponent = componentSet.defaultVariant + if (!defaultComponent) { + return [] + } + + // Get base props from defaultVariant + const codegen = new Codegen(defaultComponent) + const tree = await codegen.getTree() + + // Get pseudo-selector props (hover, active, disabled, etc.) + const selectorProps = await getSelectorPropsForGroup(componentSet, {}) + if (Object.keys(selectorProps).length > 0) { + Object.assign(tree.props, selectorProps) + } + + // Render the tree to JSX + const code = Codegen.renderTree(tree, 2) + + // No variant props needed since effect is handled via pseudo-selectors + const result: Array = [ + [componentName, renderComponent(componentName, code, {})], + ] + return result + } + /** * Generate component code for COMPONENT_SET with non-viewport variants only. */ From a3b11d10e4c296b1b28de242dc13ddcc4f1f7348 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 20:01:57 +0900 Subject: [PATCH 43/51] Add cmd --- src/code-impl.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/code-impl.ts b/src/code-impl.ts index bf99f2b..101db59 100644 --- a/src/code-impl.ts +++ b/src/code-impl.ts @@ -211,6 +211,16 @@ export function registerCodegen(ctx: typeof figma) { .map((code) => code[1]) .join('\n\n'), }, + { + title: `${node.name} - Components Responsive CLI (Bash)`, + language: 'BASH' as const, + code: generateBashCLI(responsiveComponentsCodes), + }, + { + title: `${node.name} - Components Responsive CLI (PowerShell)`, + language: 'BASH' as const, + code: generatePowerShellCLI(responsiveComponentsCodes), + }, ] : []), ...responsiveResult, From 097ae4e5b595e5700324f7207fe79fb9f5f7b3ae Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 20:04:07 +0900 Subject: [PATCH 44/51] Add cmd --- src/codegen/props/selector.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/codegen/props/selector.ts b/src/codegen/props/selector.ts index 07353ff..50e1f16 100644 --- a/src/codegen/props/selector.ts +++ b/src/codegen/props/selector.ts @@ -23,11 +23,20 @@ const shortToCssProperty: Record = { pos: 'position', } +/** + * Convert camelCase to kebab-case for CSS property names. + */ +function toKebabCase(str: string): string { + return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`) +} + /** * Convert shorthand prop names to CSS standard property names for transitionProperty. + * Also converts camelCase to kebab-case (e.g., boxShadow -> box-shadow). */ function toTransitionPropertyName(key: string): string { - return shortToCssProperty[key] || key + const mapped = shortToCssProperty[key] || key + return toKebabCase(mapped) } // 속성 이름을 유효한 TypeScript 식별자로 변환 From 673e41ceca58a51a7d8dde8bc8c10f792bc31ca9 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 20:15:31 +0900 Subject: [PATCH 45/51] Add responsive test --- .../__tests__/codegen-viewport.test.ts | 343 + src/codegen/__tests__/codegen.test.ts | 5837 ++++++++++++++++- src/codegen/utils/node-proxy.ts | 8 + 3 files changed, 6185 insertions(+), 3 deletions(-) diff --git a/src/codegen/__tests__/codegen-viewport.test.ts b/src/codegen/__tests__/codegen-viewport.test.ts index e4ad9ea..d62400d 100644 --- a/src/codegen/__tests__/codegen-viewport.test.ts +++ b/src/codegen/__tests__/codegen-viewport.test.ts @@ -355,3 +355,346 @@ describe('Codegen viewport variant', () => { expect(codes.length).toBe(2) }) }) + +describe('Codegen effect-only COMPONENT_SET', () => { + test('generates code with pseudo-selectors for effect-only component set', async () => { + // Create effect variants: default, hover, active, disabled + const defaultVariant = createComponentNode( + 'effect=default', + { effect: 'default' }, + { + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.5, g: 0.3, b: 0.9 }, + opacity: 1, + } as unknown as Paint, + ], + reactions: [ + { + trigger: { type: 'ON_HOVER' }, + actions: [ + { + type: 'NODE', + transition: { + type: 'SMART_ANIMATE', + duration: 0.3, + easing: { type: 'EASE_OUT' }, + }, + }, + ], + }, + ] as unknown as Reaction[], + }, + ) + + const hoverVariant = createComponentNode( + 'effect=hover', + { effect: 'hover' }, + { + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.4, g: 0.2, b: 0.8 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const activeVariant = createComponentNode( + 'effect=active', + { effect: 'active' }, + { + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.3, g: 0.1, b: 0.7 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const disabledVariant = createComponentNode( + 'effect=disabled', + { effect: 'disabled' }, + { + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.8, g: 0.8, b: 0.8 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const componentSet = createComponentSetNode( + 'EffectButton', + { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover', 'active', 'disabled'], + }, + }, + [defaultVariant, hoverVariant, activeVariant, disabledVariant], + ) + + const codes = await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'EffectButton', + ) + + expect(codes.length).toBe(1) + const [componentName, generatedCode] = codes[0] + expect(componentName).toBe('EffectButton') + + // Should have pseudo-selector props + expect(generatedCode).toContain('_hover') + expect(generatedCode).toContain('_active') + expect(generatedCode).toContain('_disabled') + + // Should have transition properties + expect(generatedCode).toContain('transition=') + expect(generatedCode).toContain('transitionProperty=') + + // Should NOT have effect as a prop (handled via pseudo-selectors) + expect(generatedCode).not.toContain('effect:') + }) + + test('generates code with viewport + effect variants', async () => { + // Mobile variants + const mobileDefault = createComponentNode( + 'effect=default, viewport=Mobile', + { effect: 'default', viewport: 'Mobile' }, + { + width: 150, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.5, g: 0.5, b: 0.5 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const mobileHover = createComponentNode( + 'effect=hover, viewport=Mobile', + { effect: 'hover', viewport: 'Mobile' }, + { + width: 150, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.6, g: 0.6, b: 0.6 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + // Desktop variants + const desktopDefault = createComponentNode( + 'effect=default, viewport=Desktop', + { effect: 'default', viewport: 'Desktop' }, + { + width: 200, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.5, g: 0.5, b: 0.5 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const desktopHover = createComponentNode( + 'effect=hover, viewport=Desktop', + { effect: 'hover', viewport: 'Desktop' }, + { + width: 200, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.7, g: 0.7, b: 0.7 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const componentSet = createComponentSetNode( + 'ResponsiveEffectButton', + { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover'], + }, + viewport: { + type: 'VARIANT', + defaultValue: 'Desktop', + variantOptions: ['Mobile', 'Desktop'], + }, + }, + [mobileDefault, mobileHover, desktopDefault, desktopHover], + ) + + const codes = await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'ResponsiveEffectButton', + ) + + expect(codes.length).toBe(1) + const [componentName, generatedCode] = codes[0] + expect(componentName).toBe('ResponsiveEffectButton') + + // Should have responsive width (different for mobile vs desktop) + expect(generatedCode).toContain('w={') + expect(generatedCode).toContain('"150px"') + expect(generatedCode).toContain('"200px"') + + // Should have _hover with responsive bg colors + expect(generatedCode).toContain('_hover') + }) + + test('generates code with effect + size variants', async () => { + // Size=Md variants + const mdDefault = createComponentNode( + 'effect=default, size=Md', + { effect: 'default', size: 'Md' }, + { + width: 100, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.2, g: 0.4, b: 0.8 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const mdHover = createComponentNode( + 'effect=hover, size=Md', + { effect: 'hover', size: 'Md' }, + { + width: 100, + height: 50, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.3, g: 0.5, b: 0.9 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + // Size=Sm variants + const smDefault = createComponentNode( + 'effect=default, size=Sm', + { effect: 'default', size: 'Sm' }, + { + width: 80, + height: 40, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.2, g: 0.4, b: 0.8 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const smHover = createComponentNode( + 'effect=hover, size=Sm', + { effect: 'hover', size: 'Sm' }, + { + width: 80, + height: 40, + layoutSizingHorizontal: 'FIXED', + layoutSizingVertical: 'FIXED', + fills: [ + { + type: 'SOLID', + visible: true, + color: { r: 0.3, g: 0.5, b: 0.9 }, + opacity: 1, + } as unknown as Paint, + ], + }, + ) + + const componentSet = createComponentSetNode( + 'SizeEffectButton', + { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['default', 'hover'], + }, + size: { + type: 'VARIANT', + defaultValue: 'Md', + variantOptions: ['Md', 'Sm'], + }, + }, + [mdDefault, mdHover, smDefault, smHover], + ) + + const codes = await ResponsiveCodegen.generateVariantResponsiveComponents( + componentSet, + 'SizeEffectButton', + ) + + expect(codes.length).toBe(1) + const [componentName, generatedCode] = codes[0] + expect(componentName).toBe('SizeEffectButton') + + // Should have size prop in interface + expect(generatedCode).toContain("size: 'Md' | 'Sm'") + + // Should have variant-conditional width + expect(generatedCode).toContain('w={') + expect(generatedCode).toContain('[size]') + + // Should have _hover props + expect(generatedCode).toContain('_hover') + }) +}) diff --git a/src/codegen/__tests__/codegen.test.ts b/src/codegen/__tests__/codegen.test.ts index 54ca33e..69fb20b 100644 --- a/src/codegen/__tests__/codegen.test.ts +++ b/src/codegen/__tests__/codegen.test.ts @@ -1,5 +1,7 @@ import { afterAll, describe, expect, it, test } from 'bun:test' +import { getComponentName } from '../../utils' import { Codegen } from '../Codegen' +import { ResponsiveCodegen } from '../responsive/ResponsiveCodegen' import { assembleNodeTree, type NodeData } from '../utils/node-proxy' ;(globalThis as { figma?: unknown }).figma = { @@ -28180,13 +28182,5842 @@ describe('render real world component', () => { }, ], }, + { + expected: `export function GradientButton() { + return ( +
    + + 자세히 알아보기 → + +
    + ) +}`, + nodes: [ + { + id: '351:1546', + name: 'GradientButton', + type: 'COMPONENT_SET', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '0:1', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'FIXED', + layoutSizingHorizontal: 'FIXED', + height: 110, + cornerRadius: 5, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5411764979362488, + g: 0.21960784494876862, + b: 0.9607843160629272, + }, + boundVariables: {}, + }, + ], + dashPattern: [10, 5], + strokeWeight: 1, + strokeAlign: 'INSIDE', + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + paddingTop: 0, + paddingRight: 0, + paddingBottom: 0, + paddingLeft: 0, + isAsset: false, + effects: [], + children: ['351:1547', '351:1549', '351:1551', '351:1553'], + rotation: 0, + clipsContent: true, + visible: true, + defaultVariant: '351:1551', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['active', 'default', 'disabled', 'hover'], + }, + }, + }, + { + id: '351:1547', + name: 'effect=hover', + type: 'COMPONENT', + visible: true, + parent: '351:1546', + children: ['351:1548'], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.4941176474094391, + g: 0.13333334028720856, + b: 0.8078431487083435, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.26274511218070984, + g: 0.21960784494876862, + b: 0.7921568751335144, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + strokes: [], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 6, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 4, + }, + spread: -4, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + { + type: 'DROP_SHADOW', + visible: true, + radius: 15, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 10, + }, + spread: -3, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 174, + height: 46, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '351:1549', + navigation: 'CHANGE_TO', + transition: null, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '351:1549', + navigation: 'CHANGE_TO', + transition: null, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'hover', + }, + }, + { + id: '351:1548', + name: 'Text', + type: 'TEXT', + visible: true, + parent: '351:1547', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 114, + height: 22, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '자세히 알아보기 →', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 16, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '자세히 알아보기 →', + start: 0, + end: 10, + fontSize: 16, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:34c356e42f6c53205f63e2d290404cfe897daf77,257:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '351:1549', + name: 'effect=active', + type: 'COMPONENT', + visible: true, + parent: '351:1546', + children: ['351:1550'], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.4941176474094391, + g: 0.13333334028720856, + b: 0.8078431487083435, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.26274511218070984, + g: 0.21960784494876862, + b: 0.7921568751335144, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + strokes: [], + effects: [], + opacity: 0.800000011920929, + blendMode: 'PASS_THROUGH', + width: 174, + height: 46, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + }, + }, + { + id: '351:1550', + name: 'Text', + type: 'TEXT', + visible: true, + parent: '351:1549', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 114, + height: 22, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '자세히 알아보기 →', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 16, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '자세히 알아보기 →', + start: 0, + end: 10, + fontSize: 16, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:34c356e42f6c53205f63e2d290404cfe897daf77,257:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '351:1551', + name: 'effect=default', + type: 'COMPONENT', + visible: true, + parent: '351:1546', + children: ['351:1552'], + fills: [ + { + type: 'GRADIENT_LINEAR', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + gradientStops: [ + { + color: { + r: 0.5764706134796143, + g: 0.20000000298023224, + b: 0.9176470637321472, + a: 1, + }, + position: 0, + boundVariables: {}, + }, + { + color: { + r: 0.30980393290519714, + g: 0.27450981736183167, + b: 0.8980392217636108, + a: 1, + }, + position: 1, + boundVariables: {}, + }, + ], + gradientTransform: [ + [1, 0, 0], + [0, 1, 0], + ], + }, + ], + strokes: [], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 2, + }, + spread: -2, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + { + type: 'DROP_SHADOW', + visible: true, + radius: 6, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 4, + }, + spread: -1, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 174, + height: 46, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '351:1547', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.30000001192092896, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '351:1547', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.30000001192092896, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_HOVER', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + }, + }, + { + id: '351:1552', + name: 'Text', + type: 'TEXT', + visible: true, + parent: '351:1551', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 114, + height: 22, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '자세히 알아보기 →', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 16, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '자세히 알아보기 →', + start: 0, + end: 10, + fontSize: 16, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:34c356e42f6c53205f63e2d290404cfe897daf77,257:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '351:1553', + name: 'effect=disabled', + type: 'COMPONENT', + visible: true, + parent: '351:1546', + children: ['351:1554'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.8980392217636108, + g: 0.9058823585510254, + b: 0.9215686321258545, + }, + boundVariables: { + color: + '[NodeId: VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112]', + }, + }, + ], + strokes: [], + effects: [ + { + type: 'DROP_SHADOW', + visible: true, + radius: 4, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 2, + }, + spread: -2, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + { + type: 'DROP_SHADOW', + visible: true, + radius: 6, + boundVariables: {}, + color: { + r: 0, + g: 0, + b: 0, + a: 0.10000000149011612, + }, + offset: { + x: 0, + y: 4, + }, + spread: -1, + blendMode: 'NORMAL', + showShadowBehindNode: false, + }, + ], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 174, + height: 46, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 0, + counterAxisSpacing: 0, + clipsContent: true, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 0, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'disabled', + }, + }, + { + id: '351:1554', + name: 'Text', + type: 'TEXT', + visible: true, + parent: '351:1553', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6117647290229797, + g: 0.6392157077789307, + b: 0.686274528503418, + }, + boundVariables: { + color: + '[NodeId: VariableID:447524240a084f4401116412e7f6bff5fe9ce064/14:114]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 114, + height: 22, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '자세히 알아보기 →', + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontSize: 16, + fontWeight: 700, + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'CENTER', + textAlignVertical: 'CENTER', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '자세히 알아보기 →', + start: 0, + end: 10, + fontSize: 16, + fontName: { + family: 'Pretendard', + style: 'Bold', + }, + fontWeight: 700, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'PERCENT', + value: 139.9999976158142, + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.6117647290229797, + g: 0.6392157077789307, + b: 0.686274528503418, + }, + boundVariables: { + color: + '[NodeId: VariableID:447524240a084f4401116412e7f6bff5fe9ce064/14:114]', + }, + }, + ], + textStyleId: 'S:34c356e42f6c53205f63e2d290404cfe897daf77,257:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + ], + variables: [ + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:447524240a084f4401116412e7f6bff5fe9ce064/14:114', + name: 'gray400', + }, + ], + }, + { + expected: `export interface ButtonProps { + size: 'Md' | 'Sm' + variant: 'primary' | 'white' +} + +export function Button({ size, variant }: ButtonProps) { + return ( +
    + + 참가 신청하기 + +
    + ) +}`, + nodes: [ + { + id: '536:2019', + name: 'button', + type: 'COMPONENT_SET', + inferredAutoLayout: { + layoutMode: 'VERTICAL', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + paddingBottom: 10, + counterAxisSizingMode: 'FIXED', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + children: [ + '536:2020', + '536:2022', + '536:2024', + '536:2026', + '536:2028', + '536:2030', + '536:2032', + '536:2034', + '536:2036', + '536:2038', + '536:2040', + '536:2042', + '536:2044', + '536:2046', + '536:2048', + '536:2050', + '536:2052', + '536:2054', + '536:2056', + '536:2058', + ], + primaryAxisAlignItems: 'MIN', + counterAxisAlignItems: 'MIN', + maxWidth: null, + maxHeight: null, + minWidth: null, + minHeight: null, + parent: '0:1', + layoutPositioning: 'AUTO', + layoutSizingVertical: 'HUG', + layoutSizingHorizontal: 'FIXED', + cornerRadius: 5, + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.5411764979362488, + g: 0.21960784494876862, + b: 0.9607843160629272, + }, + boundVariables: {}, + }, + ], + dashPattern: [10, 5], + strokeWeight: 1, + strokeAlign: 'INSIDE', + fills: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + isAsset: false, + effects: [], + rotation: 0, + clipsContent: true, + visible: true, + defaultVariant: '536:2020', + componentPropertyDefinitions: { + effect: { + type: 'VARIANT', + defaultValue: 'default', + variantOptions: ['active', 'default', 'hover'], + }, + viewport: { + type: 'VARIANT', + defaultValue: 'Desktop', + variantOptions: ['Desktop', 'Mobile'], + }, + size: { + type: 'VARIANT', + defaultValue: 'Md', + variantOptions: ['Md', 'Sm'], + }, + variant: { + type: 'VARIANT', + defaultValue: 'primary', + variantOptions: ['primary', 'white'], + }, + }, + }, + { + id: '536:2020', + name: 'effect=default, viewport=Desktop, size=Md, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2021'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3525640070438385, + g: 0.2507188618183136, + b: 0.17345693707466125, + }, + boundVariables: { + color: + '[NodeId: VariableID:c00308f0d374bc5a508ccdf159c175403715413b/171:260]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2022', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2022', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_HOVER', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Desktop', + size: 'Md', + variant: 'primary', + }, + }, + { + id: '536:2021', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2020', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2022', + name: 'effect=hover, viewport=Desktop, size=Md, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2023'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24198707938194275, + g: 0.17384839057922363, + b: 0.12215694040060043, + }, + boundVariables: { + color: + '[NodeId: VariableID:6fda38b621cee7138fefe2f548fb0ac32bed50d2/184:19]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2024', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2024', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'hover', + viewport: 'Desktop', + size: 'Md', + variant: 'primary', + }, + }, + { + id: '536:2023', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2022', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2024', + name: 'effect=active, viewport=Desktop, size=Md, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2025'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.19230769574642181, + g: 0.14288927614688873, + b: 0.10539940744638443, + }, + boundVariables: { + color: + '[NodeId: VariableID:e541fd0850144a6d8ce0d82c7b1cda4ab6de8634/214:234]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Desktop', + size: 'Md', + variant: 'primary', + }, + }, + { + id: '536:2025', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2024', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2026', + name: 'effect=default, viewport=Mobile, size=Md, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2027'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3525640070438385, + g: 0.2507188618183136, + b: 0.17345693707466125, + }, + boundVariables: { + color: + '[NodeId: VariableID:c00308f0d374bc5a508ccdf159c175403715413b/171:260]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 149, + height: 51, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2028', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2028', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Mobile', + size: 'Md', + variant: 'primary', + }, + }, + { + id: '536:2027', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2026', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 89, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 16, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 16, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:5ccd36654caf58d0aac160577ccf843fb83effcd,50:134', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2028', + name: 'effect=active, viewport=Mobile, size=Md, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2029'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24198707938194275, + g: 0.17384839057922363, + b: 0.12215694040060043, + }, + boundVariables: { + color: + '[NodeId: VariableID:6fda38b621cee7138fefe2f548fb0ac32bed50d2/184:19]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 149, + height: 51, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Mobile', + size: 'Md', + variant: 'primary', + }, + }, + { + id: '536:2029', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2028', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 89, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 16, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 16, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:5ccd36654caf58d0aac160577ccf843fb83effcd,50:134', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2030', + name: 'effect=default, viewport=Desktop, size=Sm, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2031'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3525640070438385, + g: 0.2507188618183136, + b: 0.17345693707466125, + }, + boundVariables: { + color: + '[NodeId: VariableID:c00308f0d374bc5a508ccdf159c175403715413b/171:260]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2032', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2032', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_HOVER', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Desktop', + size: 'Sm', + variant: 'primary', + }, + }, + { + id: '536:2031', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2030', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2032', + name: 'effect=hover, viewport=Desktop, size=Sm, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2033'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24198707938194275, + g: 0.17384839057922363, + b: 0.12215694040060043, + }, + boundVariables: { + color: + '[NodeId: VariableID:6fda38b621cee7138fefe2f548fb0ac32bed50d2/184:19]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2034', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2034', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'hover', + viewport: 'Desktop', + size: 'Sm', + variant: 'primary', + }, + }, + { + id: '536:2033', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2032', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2034', + name: 'effect=active, viewport=Desktop, size=Sm, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2035'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.19230769574642181, + g: 0.14288927614688873, + b: 0.10539940744638443, + }, + boundVariables: { + color: + '[NodeId: VariableID:e541fd0850144a6d8ce0d82c7b1cda4ab6de8634/214:234]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Desktop', + size: 'Sm', + variant: 'primary', + }, + }, + { + id: '536:2035', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2034', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2036', + name: 'effect=default, viewport=Mobile, size=Sm, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2037'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.3525640070438385, + g: 0.2507188618183136, + b: 0.17345693707466125, + }, + boundVariables: { + color: + '[NodeId: VariableID:c00308f0d374bc5a508ccdf159c175403715413b/171:260]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 110, + height: 41, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2038', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2038', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Mobile', + size: 'Sm', + variant: 'primary', + }, + }, + { + id: '536:2037', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2036', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 78, + height: 17, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 14, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:e9ebfd71c5ce53c0bee7d13e5bfbfdd3602bbc4c,214:240', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2038', + name: 'effect=active, viewport=Mobile, size=Sm, variant=primary', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2039'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.24198707938194275, + g: 0.17384839057922363, + b: 0.12215694040060043, + }, + boundVariables: { + color: + '[NodeId: VariableID:6fda38b621cee7138fefe2f548fb0ac32bed50d2/184:19]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 110, + height: 41, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Mobile', + size: 'Sm', + variant: 'primary', + }, + }, + { + id: '536:2039', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2038', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 78, + height: 17, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 14, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: {}, + }, + ], + textStyleId: 'S:e9ebfd71c5ce53c0bee7d13e5bfbfdd3602bbc4c,214:240', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2040', + name: 'effect=default, viewport=Desktop, size=Md, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2041'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:66d5566c6c181693c4432ba889ac096a2cc5ae87/10:20]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2042', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2042', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_HOVER', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Desktop', + size: 'Md', + variant: 'white', + }, + }, + { + id: '536:2041', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2040', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2042', + name: 'effect=hover, viewport=Desktop, size=Md, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2043'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9647058844566345, + g: 0.9529411792755127, + b: 0.929411768913269, + }, + boundVariables: { + color: + '[NodeId: VariableID:de57c6fe2f9f2d0bac7251b1cb0473a0e25aa741/11:0]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2044', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2044', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'hover', + viewport: 'Desktop', + size: 'Md', + variant: 'white', + }, + }, + { + id: '536:2043', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2042', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2044', + name: 'effect=active, viewport=Desktop, size=Md, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2045'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9150639772415161, + g: 0.8950223922729492, + b: 0.8549392223358154, + }, + boundVariables: { + color: + '[NodeId: VariableID:aaa26483266ca762b4f299e7f324b88cd3986a3f/218:287]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 191, + height: 64, + rotation: 0, + cornerRadius: 12, + topLeftRadius: 12, + topRightRadius: 12, + bottomLeftRadius: 12, + bottomRightRadius: 12, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 40, + paddingRight: 40, + paddingTop: 20, + paddingBottom: 20, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Desktop', + size: 'Md', + variant: 'white', + }, + }, + { + id: '536:2045', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2044', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 111, + height: 24, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 20, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 20, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:f067bb28d04c8d3b5404525c3bb062c7a5c3f8c6,50:23', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2046', + name: 'effect=default, viewport=Mobile, size=Md, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2047'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:66d5566c6c181693c4432ba889ac096a2cc5ae87/10:20]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 149, + height: 51, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2048', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2048', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Mobile', + size: 'Md', + variant: 'white', + }, + }, + { + id: '536:2047', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2046', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 89, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 16, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 16, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:5ccd36654caf58d0aac160577ccf843fb83effcd,50:134', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2048', + name: 'effect=active, viewport=Mobile, size=Md, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2049'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9647058844566345, + g: 0.9529411792755127, + b: 0.929411768913269, + }, + boundVariables: { + color: + '[NodeId: VariableID:de57c6fe2f9f2d0bac7251b1cb0473a0e25aa741/11:0]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 149, + height: 51, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 30, + paddingRight: 30, + paddingTop: 16, + paddingBottom: 16, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Mobile', + size: 'Md', + variant: 'white', + }, + }, + { + id: '536:2049', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2048', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 89, + height: 19, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 16, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 16, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:5ccd36654caf58d0aac160577ccf843fb83effcd,50:134', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2050', + name: 'effect=default, viewport=Desktop, size=Sm, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2051'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:66d5566c6c181693c4432ba889ac096a2cc5ae87/10:20]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2052', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2052', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_HOVER', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Desktop', + size: 'Sm', + variant: 'white', + }, + }, + { + id: '536:2051', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2050', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2052', + name: 'effect=hover, viewport=Desktop, size=Sm, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2053'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9647058844566345, + g: 0.9529411792755127, + b: 0.929411768913269, + }, + boundVariables: { + color: + '[NodeId: VariableID:de57c6fe2f9f2d0bac7251b1cb0473a0e25aa741/11:0]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2054', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2054', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'hover', + viewport: 'Desktop', + size: 'Sm', + variant: 'white', + }, + }, + { + id: '536:2053', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2052', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2054', + name: 'effect=active, viewport=Desktop, size=Sm, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2055'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9150639772415161, + g: 0.8950223922729492, + b: 0.8549392223358154, + }, + boundVariables: { + color: + '[NodeId: VariableID:aaa26483266ca762b4f299e7f324b88cd3986a3f/218:287]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 123, + height: 46, + rotation: 0, + cornerRadius: 10, + topLeftRadius: 10, + topRightRadius: 10, + bottomLeftRadius: 10, + bottomRightRadius: 10, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 14, + paddingBottom: 14, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Desktop', + size: 'Sm', + variant: 'white', + }, + }, + { + id: '536:2055', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2054', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 83, + height: 18, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 15, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 15, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:2c8bbaddeba2edbdc9706ea2efbd3b2d46aac5fa,214:239', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2056', + name: 'effect=default, viewport=Mobile, size=Sm, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2057'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 1, + g: 1, + b: 1, + }, + boundVariables: { + color: + '[NodeId: VariableID:66d5566c6c181693c4432ba889ac096a2cc5ae87/10:20]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 110, + height: 41, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [ + { + action: { + type: 'NODE', + destinationId: '536:2058', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + actions: [ + { + type: 'NODE', + destinationId: '536:2058', + navigation: 'CHANGE_TO', + transition: { + type: 'SMART_ANIMATE', + easing: { + type: 'EASE_OUT', + }, + duration: 0.20000000298023224, + }, + resetVideoPosition: false, + }, + ], + trigger: { + type: 'ON_PRESS', + }, + }, + ], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'default', + viewport: 'Mobile', + size: 'Sm', + variant: 'white', + }, + }, + { + id: '536:2057', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2056', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 78, + height: 17, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 14, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:e9ebfd71c5ce53c0bee7d13e5bfbfdd3602bbc4c,214:240', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + { + id: '536:2058', + name: 'effect=active, viewport=Mobile, size=Sm, variant=white', + type: 'COMPONENT', + visible: true, + parent: '536:2019', + children: ['536:2059'], + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.9647058844566345, + g: 0.9529411792755127, + b: 0.929411768913269, + }, + boundVariables: { + color: + '[NodeId: VariableID:de57c6fe2f9f2d0bac7251b1cb0473a0e25aa741/11:0]', + }, + }, + ], + strokes: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 110, + height: 41, + rotation: 0, + cornerRadius: 8, + topLeftRadius: 8, + topRightRadius: 8, + bottomLeftRadius: 8, + bottomRightRadius: 8, + layoutMode: 'HORIZONTAL', + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + itemSpacing: 10, + counterAxisSpacing: 0, + clipsContent: false, + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + inferredAutoLayout: { + layoutMode: 'HORIZONTAL', + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + counterAxisSizingMode: 'AUTO', + primaryAxisSizingMode: 'AUTO', + primaryAxisAlignItems: 'CENTER', + counterAxisAlignItems: 'CENTER', + layoutAlign: 'INHERIT', + layoutGrow: 0, + itemSpacing: 10, + layoutPositioning: 'AUTO', + }, + strokeWeight: 1, + strokeTopWeight: 1, + strokeBottomWeight: 1, + strokeLeftWeight: 1, + strokeRightWeight: 1, + strokeAlign: 'INSIDE', + dashPattern: [], + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + gridColumnCount: 0, + variantProperties: { + effect: 'active', + viewport: 'Mobile', + size: 'Sm', + variant: 'white', + }, + }, + { + id: '536:2059', + name: '참가 신청하기', + type: 'TEXT', + visible: true, + parent: '536:2058', + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + strokes: [], + effects: [], + opacity: 1, + blendMode: 'PASS_THROUGH', + width: 78, + height: 17, + rotation: 0, + layoutAlign: 'INHERIT', + layoutGrow: 0, + layoutSizingHorizontal: 'HUG', + layoutSizingVertical: 'HUG', + layoutPositioning: 'AUTO', + isAsset: false, + reactions: [], + minWidth: null, + maxWidth: null, + minHeight: null, + maxHeight: null, + strokeWeight: 1, + strokeAlign: 'OUTSIDE', + dashPattern: [], + characters: '참가 신청하기', + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontSize: 14, + fontWeight: 500, + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + textAutoResize: 'WIDTH_AND_HEIGHT', + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textTruncation: 'DISABLED', + gridColumnAnchorIndex: -1, + gridRowAnchorIndex: -1, + styledTextSegments: [ + { + characters: '참가 신청하기', + start: 0, + end: 7, + fontSize: 14, + fontName: { + family: 'Noto Sans KR', + style: 'Medium', + }, + fontWeight: 500, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { + unit: 'AUTO', + }, + letterSpacing: { + unit: 'PERCENT', + value: -4, + }, + fills: [ + { + type: 'SOLID', + visible: true, + opacity: 1, + blendMode: 'NORMAL', + color: { + r: 0.48557692766189575, + g: 0.2665456235408783, + b: 0.1003836914896965, + }, + boundVariables: { + color: + '[NodeId: VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277]', + }, + }, + ], + textStyleId: 'S:e9ebfd71c5ce53c0bee7d13e5bfbfdd3602bbc4c,214:240', + fillStyleId: '', + listOptions: { + type: 'NONE', + }, + indentation: 0, + hyperlink: null, + }, + ], + }, + ], + variables: [ + { + id: 'VariableID:54a5e49a31d11583de0ae1faa3ec4bf26bd03a86/14:112', + name: 'gray200', + }, + { + id: 'VariableID:447524240a084f4401116412e7f6bff5fe9ce064/14:114', + name: 'gray400', + }, + { + id: 'VariableID:c00308f0d374bc5a508ccdf159c175403715413b/171:260', + name: 'primary', + }, + { + id: 'VariableID:6fda38b621cee7138fefe2f548fb0ac32bed50d2/184:19', + name: 'primaryBold', + }, + { + id: 'VariableID:e541fd0850144a6d8ce0d82c7b1cda4ab6de8634/214:234', + name: 'primaryExBold', + }, + { + id: 'VariableID:66d5566c6c181693c4432ba889ac096a2cc5ae87/10:20', + name: 'containerBackground', + }, + { + id: 'VariableID:19c4aef6f32a229732ad97d11bc8a6e9c6d7654e/171:277', + name: 'primaryAccent', + }, + { + id: 'VariableID:de57c6fe2f9f2d0bac7251b1cb0473a0e25aa741/11:0', + name: 'background', + }, + { + id: 'VariableID:aaa26483266ca762b4f299e7f324b88cd3986a3f/218:287', + name: 'backgroundBold', + }, + ], + }, ] as const)('$expected', async ({ expected, nodes, variables }) => { const root = assembleNodeTree( nodes as unknown as NodeData[], variables as { id: string; name: string }[] | undefined, ) - const codegen = new Codegen(root as unknown as SceneNode) - await codegen.run() - expect(codegen.getCode()).toBe(expected) + + // For COMPONENT_SET, use ResponsiveCodegen to get component code with pseudo-selectors + if (root.type === 'COMPONENT_SET') { + const componentName = getComponentName( + root as unknown as ComponentSetNode, + ) + const codes = await ResponsiveCodegen.generateVariantResponsiveComponents( + root as unknown as ComponentSetNode, + componentName, + ) + expect(codes.length).toBeGreaterThan(0) + expect(codes[0][1]).toBe(expected) + } else { + const codegen = new Codegen(root as unknown as SceneNode) + await codegen.run() + expect(codegen.getCode()).toBe(expected) + } }) }) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index fd05f1a..ac77f86 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -583,6 +583,14 @@ export function assembleNodeTree( } // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) + // defaultVariant 연결 (COMPONENT_SET용) + if (typeof node.defaultVariant === 'string') { + const defaultVariantNode = nodeMap.get(node.defaultVariant) + if (defaultVariantNode) { + node.defaultVariant = defaultVariantNode + } + } + // fills/strokes의 boundVariables 처리 // boundVariables.color가 문자열 ID인 경우 { id: '...' } 객체로 변환 const processBoundVariables = (paints: unknown[]) => { From 74eeed242f78636e3287d63805b06edaf50e5090 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 29 Dec 2025 20:49:55 +0900 Subject: [PATCH 46/51] Add responsive test --- .../__tests__/ResponsiveCodegen.test.ts | 163 +++++++ .../__tests__/mergePropsToVariant.test.ts | 18 + .../utils/__tests__/node-proxy.test.ts | 446 +++++++++++++++++- .../utils/__tests__/props-to-str.test.ts | 218 +++++++++ 4 files changed, 844 insertions(+), 1 deletion(-) diff --git a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts index adb6882..8a48571 100644 --- a/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts +++ b/src/codegen/responsive/__tests__/ResponsiveCodegen.test.ts @@ -712,6 +712,169 @@ describe('ResponsiveCodegen', () => { }) }) + describe('generateVariantMergedCode', () => { + it('merges trees across both viewport and variant dimensions', () => { + const generator = new ResponsiveCodegen(null) + + // Trees by variant and breakpoint: + // status=scroll => { mobile: tree1, pc: tree2 } + // status=default => { mobile: tree3, pc: tree4 } + const treesByVariantAndBreakpoint = new Map< + string, + Map<'mobile' | 'tablet' | 'pc', NodeTree> + >([ + [ + 'scroll', + new Map<'mobile' | 'tablet' | 'pc', NodeTree>([ + [ + 'mobile', + { + component: 'Box', + props: { w: '100px', h: '50px' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'pc', + { + component: 'Box', + props: { w: '200px', h: '50px' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]), + ], + [ + 'default', + new Map<'mobile' | 'tablet' | 'pc', NodeTree>([ + [ + 'mobile', + { + component: 'Box', + props: { w: '150px', h: '50px' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'pc', + { + component: 'Box', + props: { w: '250px', h: '50px' }, + children: [], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]), + ], + ]) + + const result = generator.generateVariantMergedCode( + 'status', + treesByVariantAndBreakpoint, + 0, + ) + + // h should be same value (50px) - no responsive, no variant conditional + expect(result).toContain('"h":"50px"') + // w should have both responsive (mobile vs pc) AND variant conditional + // scroll: mobile=100px, pc=200px => ["100px", null, null, null, "200px"] + // default: mobile=150px, pc=250px => ["150px", null, null, null, "250px"] + expect(result).toContain('scroll') + expect(result).toContain('default') + expect(result).toContain('status') // variantKey + }) + + it('merges children across viewport and variant dimensions', () => { + const generator = new ResponsiveCodegen(null) + + // Child that exists only on mobile for scroll, and on both for default + const mobileOnlyChild: NodeTree = { + component: 'Text', + props: { id: 'MobileChild' }, + children: [], + nodeType: 'TEXT', + nodeName: 'MobileChild', + } + + const treesByVariantAndBreakpoint = new Map< + string, + Map<'mobile' | 'tablet' | 'pc', NodeTree> + >([ + [ + 'scroll', + new Map<'mobile' | 'tablet' | 'pc', NodeTree>([ + [ + 'mobile', + { + component: 'Box', + props: {}, + children: [mobileOnlyChild], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'pc', + { + component: 'Box', + props: {}, + children: [], // No child on pc + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]), + ], + [ + 'default', + new Map<'mobile' | 'tablet' | 'pc', NodeTree>([ + [ + 'mobile', + { + component: 'Box', + props: {}, + children: [ + { ...mobileOnlyChild, props: { id: 'DefaultChild' } }, + ], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + [ + 'pc', + { + component: 'Box', + props: {}, + children: [ + { ...mobileOnlyChild, props: { id: 'DefaultChild' } }, + ], + nodeType: 'FRAME', + nodeName: 'Root', + }, + ], + ]), + ], + ]) + + const result = generator.generateVariantMergedCode( + 'status', + treesByVariantAndBreakpoint, + 0, + ) + + // Should contain the children's id values + expect(result).toContain('MobileChild') + expect(result).toContain('DefaultChild') + }) + }) + describe('generateVariantResponsiveComponents', () => { it('handles component set with only non-viewport variants', async () => { const componentSet = { diff --git a/src/codegen/responsive/__tests__/mergePropsToVariant.test.ts b/src/codegen/responsive/__tests__/mergePropsToVariant.test.ts index 4316f4b..2d627ab 100644 --- a/src/codegen/responsive/__tests__/mergePropsToVariant.test.ts +++ b/src/codegen/responsive/__tests__/mergePropsToVariant.test.ts @@ -123,6 +123,24 @@ describe('mergePropsToVariant', () => { expanded: '300px', }) }) + + it('handles pseudo-selector present in only some variants', () => { + // This test covers the else branch at line 445-448 + // where a variant doesn't have the pseudo-selector prop + const input = new Map([ + ['scroll', { _hover: { bg: 'red' } }], + ['default', { w: '100px' }], // no _hover pseudo-selector + ]) + const result = mergePropsToVariant('status', input) + + // _hover should be merged with variant conditionals + // scroll variant has bg: 'red', default variant gets empty object (null for inner props) + expect(result._hover).toBeDefined() + const hoverProp = result._hover as Record + expect(isVariantPropValue(hoverProp.bg)).toBe(true) + const bgProp = hoverProp.bg as ReturnType + expect(bgProp.values).toEqual({ scroll: 'red' }) + }) }) describe('isVariantPropValue', () => { diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index f46dfba..fc1ace4 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'bun:test' -import { nodeProxyTracker } from '../node-proxy' +import { assembleNodeTree, nodeProxyTracker } from '../node-proxy' // Mock SceneNode function createMockNode(overrides: Partial = {}): SceneNode { @@ -162,4 +162,448 @@ describe('nodeProxyTracker', () => { expect(log1?.properties.some((p) => p.key === 'width')).toBe(true) expect(log2?.properties.some((p) => p.key === 'height')).toBe(true) }) + + test('should track TEXT node with styledTextSegments when accessed as child', () => { + // TEXT node with getStyledTextSegments + const textNode = { + id: 'text-node-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Hello World', + fontSize: 16, + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + lineHeight: { value: 24, unit: 'PIXELS' }, + letterSpacing: { value: 0, unit: 'PIXELS' }, + textAlignHorizontal: 'LEFT', + textAlignVertical: 'TOP', + textAutoResize: 'WIDTH_AND_HEIGHT', + textTruncation: 'DISABLED', + maxLines: null, + visible: true, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 }, opacity: 1 }], + getStyledTextSegments: () => [ + { + start: 0, + end: 11, + characters: 'Hello World', + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + fontSize: 16, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: { value: 24, unit: 'PIXELS' }, + letterSpacing: { value: 0, unit: 'PIXELS' }, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 }, opacity: 1 }], + textStyleId: '', + fillStyleId: '', + listOptions: { type: 'NONE' }, + indentation: 0, + hyperlink: null, + }, + ], + } as unknown as SceneNode + + // Parent frame containing the text node + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [textNode], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(parentNode) + + // Access children to trigger trackNodeRecursively on the TEXT node + const _children = (wrapped as unknown as FrameNode).children + + const log = nodeProxyTracker.getAccessLog('text-node-1') + expect(log).toBeDefined() + expect(log?.nodeType).toBe('TEXT') + + // Check that styledTextSegments was tracked + const segmentsProp = log?.properties.find( + (p) => p.key === 'styledTextSegments', + ) + expect(segmentsProp).toBeDefined() + expect(Array.isArray(segmentsProp?.value)).toBe(true) + }) + + test('should track children nodes recursively', () => { + const childNode = createMockNode({ + id: 'child-1', + name: 'ChildNode', + type: 'FRAME', + width: 50, + height: 50, + }) + + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [childNode], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(parentNode) + + // Access children to trigger trackNodeRecursively + const _children = (wrapped as unknown as FrameNode).children + + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(2) + + const parentLog = nodeProxyTracker.getAccessLog('parent-1') + const childLog = nodeProxyTracker.getAccessLog('child-1') + + expect(parentLog).toBeDefined() + expect(childLog).toBeDefined() + + // Child should have properties tracked + expect(childLog?.properties.length).toBeGreaterThan(0) + }) + + test('should return flat node list with toNodeList', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + + const _width = wrapped.width + const _height = wrapped.height + const _name = wrapped.name + + const nodeList = nodeProxyTracker.toNodeList() + expect(nodeList.length).toBe(1) + + const nodeData = nodeList[0] + expect(nodeData.id).toBe('test-node-1') + expect(nodeData.name).toBe('TestNode') + expect(nodeData.type).toBe('FRAME') + expect(nodeData.width).toBe(100) + expect(nodeData.height).toBe(200) + }) + + test('should resolve node references in toNodeList', () => { + const childNode = createMockNode({ + id: 'child-1', + name: 'ChildNode', + }) + + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [childNode], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(parentNode) + const _children = (wrapped as unknown as FrameNode).children + + const nodeList = nodeProxyTracker.toNodeList() + expect(nodeList.length).toBe(2) + + // Check that node references are resolved to IDs + const parentData = nodeList.find((n) => n.id === 'parent-1') + expect(parentData).toBeDefined() + expect(Array.isArray(parentData?.children)).toBe(true) + }) + + test('should handle filterByRoot with nested children', () => { + const grandChild = createMockNode({ + id: 'grandchild-1', + name: 'GrandChild', + width: 25, + }) + + const childNode = { + ...createMockNode({ + id: 'child-1', + name: 'ChildNode', + width: 50, + }), + children: [grandChild], + } as unknown as SceneNode + + const parentNode = { + ...createMockNode({ id: 'parent-1', name: 'ParentNode' }), + children: [childNode], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(parentNode) + const children = (wrapped as unknown as FrameNode).children + const _grandChildren = (children[0] as unknown as FrameNode).children + + // All nodes should be tracked + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(3) + + // Test filterByRoot functionality via toTestCaseFormatWithVariables + // This accesses the internal filterByRoot which uses collectDescendants + }) + + test('should serialize array-like objects correctly', () => { + // Test with gradientTransform (array-like object with numeric keys) + const nodeWithGradient = { + ...createMockNode({ id: 'gradient-node', name: 'GradientNode' }), + fills: [ + { + type: 'GRADIENT_LINEAR', + gradientTransform: { + '0': [1, 0, 0], + '1': [0, 1, 0], + }, + gradientStops: [ + { position: 0, color: { r: 1, g: 0, b: 0, a: 1 } }, + { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } }, + ], + }, + ], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithGradient) + const _fills = (wrapped as unknown as FrameNode).fills + + const log = nodeProxyTracker.getAccessLog('gradient-node') + expect(log).toBeDefined() + + const fillsProp = log?.properties.find((p) => p.key === 'fills') + expect(fillsProp).toBeDefined() + expect(Array.isArray(fillsProp?.value)).toBe(true) + }) + + test('should track deeply nested children recursively', () => { + // Create a 3-level deep hierarchy + const level3 = createMockNode({ + id: 'level-3', + name: 'Level3', + type: 'FRAME', + }) + + const level2 = { + ...createMockNode({ + id: 'level-2', + name: 'Level2', + type: 'FRAME', + }), + children: [level3], + } as unknown as SceneNode + + const level1 = { + ...createMockNode({ + id: 'level-1', + name: 'Level1', + type: 'FRAME', + }), + children: [level2], + } as unknown as SceneNode + + const root = { + ...createMockNode({ + id: 'root', + name: 'Root', + type: 'FRAME', + }), + children: [level1], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(root) + + // Access children at each level + const children1 = (wrapped as unknown as FrameNode).children + const children2 = (children1[0] as unknown as FrameNode).children + const _children3 = (children2[0] as unknown as FrameNode).children + + // All 4 levels should be tracked + const logs = nodeProxyTracker.getAllAccessLogs() + expect(logs.length).toBe(4) + + expect(nodeProxyTracker.getAccessLog('root')).toBeDefined() + expect(nodeProxyTracker.getAccessLog('level-1')).toBeDefined() + expect(nodeProxyTracker.getAccessLog('level-2')).toBeDefined() + expect(nodeProxyTracker.getAccessLog('level-3')).toBeDefined() + }) + + test('toTestCaseFormat should filter nodes by rootId', () => { + // Create a hierarchy with 3 nodes + const child = createMockNode({ + id: 'child-1', + name: 'Child', + type: 'FRAME', + }) + + const parent = { + ...createMockNode({ + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + }), + children: [child], + } as unknown as SceneNode + + const sibling = createMockNode({ + id: 'sibling-1', + name: 'Sibling', + type: 'FRAME', + }) + + // Wrap and access all nodes + const wrappedParent = nodeProxyTracker.wrap(parent) + const _children = (wrappedParent as unknown as FrameNode).children + nodeProxyTracker.wrap(sibling) + const _siblingWidth = sibling.width + + // All 3 nodes should be tracked + expect(nodeProxyTracker.getAllAccessLogs().length).toBe(3) + + // Filter by parent-1 should only include parent and child + const filteredNodes = nodeProxyTracker.toTestCaseFormat('parent-1') + expect(filteredNodes.length).toBe(2) + + const nodeIds = filteredNodes.map((n) => n.id) + expect(nodeIds).toContain('parent-1') + expect(nodeIds).toContain('child-1') + expect(nodeIds).not.toContain('sibling-1') + }) + + test('toTestCaseFormat should return all nodes when rootId not found', () => { + const node = createMockNode() + const wrapped = nodeProxyTracker.wrap(node) + const _width = wrapped.width + + // Filter by non-existent rootId should return all nodes + const nodes = nodeProxyTracker.toTestCaseFormat('non-existent-id') + expect(nodes.length).toBe(1) + }) + + test('toTestCaseFormat should include SECTION parent', () => { + const child = createMockNode({ + id: 'child-1', + name: 'Child', + type: 'FRAME', + }) + + const sectionParent = { + id: 'section-1', + name: 'Section', + type: 'SECTION', + children: [child], + } as unknown as SceneNode + + // Add parent reference + ;(child as unknown as { parent: SceneNode }).parent = sectionParent + + // Wrap and access nodes + const wrappedSection = nodeProxyTracker.wrap(sectionParent) + const _children = (wrappedSection as unknown as SectionNode).children + + // Filter by child-1 should include the SECTION parent + const filteredNodes = nodeProxyTracker.toTestCaseFormat('child-1') + const nodeTypes = filteredNodes.map((n) => n.type) + expect(nodeTypes).toContain('FRAME') + expect(nodeTypes).toContain('SECTION') + }) + + test('toTestCaseFormat handles deeply nested children filtering', () => { + // Create a deep hierarchy: root -> level1 -> level2 -> level3 + const level3 = createMockNode({ + id: 'level-3', + name: 'Level3', + type: 'FRAME', + }) + + const level2 = { + ...createMockNode({ + id: 'level-2', + name: 'Level2', + type: 'FRAME', + }), + children: [level3], + } as unknown as SceneNode + + const level1 = { + ...createMockNode({ + id: 'level-1', + name: 'Level1', + type: 'FRAME', + }), + children: [level2], + } as unknown as SceneNode + + const root = { + ...createMockNode({ + id: 'root', + name: 'Root', + type: 'FRAME', + }), + children: [level1], + } as unknown as SceneNode + + // Track all nodes + const wrapped = nodeProxyTracker.wrap(root) + const children1 = (wrapped as unknown as FrameNode).children + const children2 = (children1[0] as unknown as FrameNode).children + const _children3 = (children2[0] as unknown as FrameNode).children + + // Filter by level-1 should include level-1, level-2, level-3 but not root + const filteredNodes = nodeProxyTracker.toTestCaseFormat('level-1') + + const nodeIds = filteredNodes.map((n) => n.id) + expect(nodeIds).toContain('level-1') + expect(nodeIds).toContain('level-2') + expect(nodeIds).toContain('level-3') + expect(nodeIds).not.toContain('root') + }) + + test('should handle array-like objects in serializeArray', () => { + // Test serializeArray with isArrayLikeObject and arrayLikeToArray + // gradientTransform is an array-like object (numeric keys) inside the fills array items + // To hit arrayLikeToArray, we need the array-like object to be a direct item in an array + const nodeWithArrayLike = { + ...createMockNode({ id: 'array-like-node', name: 'ArrayLikeNode' }), + fills: [ + // The fills array itself has array-like object as item + { + '0': 'value0', + '1': 'value1', + '2': 'value2', + }, + ], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithArrayLike) + const _fills = (wrapped as unknown as FrameNode).fills + + const log = nodeProxyTracker.getAccessLog('array-like-node') + expect(log).toBeDefined() + + const fillsProp = log?.properties.find((p) => p.key === 'fills') + expect(fillsProp).toBeDefined() + + // Check that array-like object is converted to array + const fills = fillsProp?.value as unknown[] + expect(Array.isArray(fills)).toBe(true) + + // The first item should now be an array ['value0', 'value1', 'value2'] + expect(Array.isArray(fills[0])).toBe(true) + expect(fills[0]).toEqual(['value0', 'value1', 'value2']) + }) + + test('should add getMainComponentAsync mock to INSTANCE node via assembleNodeTree', async () => { + // Create node data for an INSTANCE node + const nodes = [ + { + id: 'instance-1', + name: 'InstanceNode', + type: 'INSTANCE', + mainComponent: null, + }, + ] + + // assembleNodeTree processes nodes and adds getMainComponentAsync to INSTANCE nodes + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('INSTANCE') + + // The prepared node should have getMainComponentAsync method + expect(typeof rootNode.getMainComponentAsync).toBe('function') + + // Call the method and verify it returns null + const getMainComponentAsync = + rootNode.getMainComponentAsync as () => Promise + const mainComponent = await getMainComponentAsync() + expect(mainComponent).toBeNull() + }) }) diff --git a/src/codegen/utils/__tests__/props-to-str.test.ts b/src/codegen/utils/__tests__/props-to-str.test.ts index 0b6e724..5b2311c 100644 --- a/src/codegen/utils/__tests__/props-to-str.test.ts +++ b/src/codegen/utils/__tests__/props-to-str.test.ts @@ -177,4 +177,222 @@ describe('propsToString', () => { expect(res).toContain('scroll: Symbol(test)') expect(res).toContain('default: "20px"') }) + + test('handles pseudo-selector prop with nested VariantPropValue', () => { + // This tests _hover prop with VariantPropValue inside object + // Covers lines 137-140 (formatObjectValue handling VariantPropValue) + const nestedVariantProp = createVariantPropValue('variant', { + primary: '#FF0000', + white: '#0000FF', + }) + const res = propsToString({ + _hover: { bg: nestedVariantProp }, + }) + expect(res).toContain('_hover={') + expect(res).toContain('"bg":') + expect(res).toContain('primary: "#FF0000"') + expect(res).toContain('white: "#0000FF"') + expect(res).toContain('[variant]') + }) + + test('handles pseudo-selector prop with nested object containing VariantPropValue', () => { + // Tests deep nesting where VariantPropValue is inside a nested object + // Covers line 93 (formatValueWithIndent handling VariantPropValue) + const nestedVariantProp = createVariantPropValue('size', { + sm: { x: 1, y: 2 }, + lg: { x: 3, y: 4 }, + }) + const res = propsToString({ + _active: { transform: nestedVariantProp }, + }) + expect(res).toContain('_active={') + expect(res).toContain('"transform":') + expect(res).toContain('sm:') + expect(res).toContain('lg:') + expect(res).toContain('[size]') + }) + + test('handles VariantPropValue with single entry (inline format)', () => { + // Single entry should use inline format + const variantProp = createVariantPropValue('status', { + scroll: '10px', + }) + const res = propsToString({ w: variantProp }) + // Single entry with primitive value uses inline format + expect(res).toContain('{ scroll: "10px" }[status]') + }) + + test('handles VariantPropValue with null values', () => { + const variantProp = createVariantPropValue('status', { + scroll: null, + default: '20px', + }) + const res = propsToString({ w: variantProp }) + expect(res).toContain('scroll: null') + expect(res).toContain('default: "20px"') + }) + + test('handles pseudo-selector prop with deeply nested objects', () => { + // Tests formatObjectValue recursively handling nested objects + // Covers lines 138-140 (typeof value === 'object' branch) + const res = propsToString({ + _hover: { + transform: { scale: { x: 1.1, y: 1.1 } }, + }, + }) + expect(res).toContain('_hover={') + expect(res).toContain('"transform":') + expect(res).toContain('"scale":') + expect(res).toContain('"x": 1.1') + expect(res).toContain('"y": 1.1') + }) + + test('handles pseudo-selector prop with array value inside object', () => { + // Tests formatObjectValue handling array values + // Covers lines 131-133 (Array.isArray branch in formatObjectValue) + const res = propsToString({ + _hover: { + padding: [10, 20, 10, 20], + }, + }) + expect(res).toContain('_hover={') + expect(res).toContain('"padding": [10, 20, 10, 20]') + }) + + test('handles VariantPropValue with nested VariantPropValue in multiline format', () => { + // Tests formatValueWithIndent handling VariantPropValue + // Covers line 93 (isVariantPropValue branch in formatValueWithIndent) + const innerVariant = createVariantPropValue('size', { + sm: '10px', + lg: '20px', + }) + const outerVariant = createVariantPropValue('status', { + scroll: innerVariant, + default: '30px', + }) + const res = propsToString({ w: outerVariant }) + // Outer variant should contain inner variant with [size] access + expect(res).toContain('[status]') + expect(res).toContain('[size]') + expect(res).toContain('scroll:') + expect(res).toContain('default: "30px"') + }) + + test('valueToJsxString handles number and boolean in inline format', () => { + // Tests lines 11-12 (number/boolean handling in valueToJsxString) + // Single entry avoids multiline format, so valueToJsxString is used + const variantPropNumber = createVariantPropValue('status', { + active: 42, + }) + const res1 = propsToString({ count: variantPropNumber }) + expect(res1).toContain('{ active: 42 }[status]') + + const variantPropBool = createVariantPropValue('status', { + active: true, + }) + const res2 = propsToString({ visible: variantPropBool }) + expect(res2).toContain('{ active: true }[status]') + }) + + test('valueToJsxString handles array in inline format', () => { + // Tests lines 14-16 (array handling in valueToJsxString) + // Single entry avoids multiline format, so valueToJsxString is used + const variantProp = createVariantPropValue('status', { + active: [1, 2, 3], + }) + const res = propsToString({ items: variantProp }) + // Single entry with array value still uses multiline format due to needsMultilineFormat check + expect(res).toContain('[status]') + expect(res).toContain('active:') + }) + + test('valueToJsxString handles nested VariantPropValue in inline format', () => { + // Tests lines 19-20 (VariantPropValue handling in valueToJsxString) + // This creates a VariantPropValue inside another, but only 1 entry + const inner = createVariantPropValue('size', { + sm: '10px', + }) + const outer = createVariantPropValue('status', { + active: inner, + }) + const res = propsToString({ w: outer }) + expect(res).toContain('[status]') + expect(res).toContain('[size]') + }) + + test('valueToJsxString handles plain object in inline format', () => { + // Tests lines 22-23 (plain object handling in valueToJsxString) + // Single entry with object triggers multiline due to needsMultilineFormat + const variantProp = createVariantPropValue('status', { + active: { x: 1 }, + }) + const res = propsToString({ pos: variantProp }) + expect(res).toContain('[status]') + expect(res).toContain('active:') + expect(res).toContain('"x": 1') + }) + + test('valueToJsxString handles nested arrays with complex items', () => { + // Tests valueToJsxString branches when called from formatValueWithIndent's array handling + // formatValueWithIndent calls valueToJsxString for each array item (line 89) + // This tests lines 14-16 (array branch) and 22-23 (object branch) in valueToJsxString + const variantProp = createVariantPropValue('status', { + scroll: [ + [1, 2], + [3, 4], + ], // nested array - hits line 14-16 + default: [{ a: 1 }, { b: 2 }], // array of objects - hits line 22-23 + }) + const res = propsToString({ items: variantProp }) + expect(res).toContain('[status]') + expect(res).toContain('scroll:') + expect(res).toContain('default:') + // Nested arrays should be formatted + expect(res).toContain('[[1, 2], [3, 4]]') + // Objects inside array should be JSON stringified + expect(res).toContain('[{"a":1}, {"b":2}]') + }) + + test('valueToJsxString handles array with VariantPropValue items', () => { + // Tests line 19-20 (VariantPropValue branch) in valueToJsxString + // when called from formatValueWithIndent's array handling + const innerVariant = createVariantPropValue('size', { + sm: 'small', + lg: 'large', + }) + const variantProp = createVariantPropValue('status', { + active: [innerVariant, 'text'], // array containing VariantPropValue + inactive: ['just', 'strings'], + }) + const res = propsToString({ data: variantProp }) + expect(res).toContain('[status]') + expect(res).toContain('[size]') + expect(res).toContain('active:') + expect(res).toContain('inactive:') + }) + + test('valueToJsxString handles fallback to String() for unknown types', () => { + // Tests line 25 (fallback case) in valueToJsxString + // Functions and other exotic types fall through to String() + const fn = function testFn() {} + const variantProp = createVariantPropValue('status', { + active: [fn as unknown as string], // function in array + inactive: ['normal'], + }) + const res = propsToString({ cb: variantProp }) + expect(res).toContain('[status]') + expect(res).toContain('function') + }) + + test('formatObjectValue handles function value in pseudo-selector', () => { + // Tests line 140-141 (fallback case) in formatObjectValue + // This covers the String(value) fallback for non-standard types + const fn = function testHandler() {} + const res = propsToString({ + _hover: { onClick: fn as unknown as string }, + }) + expect(res).toContain('_hover={') + expect(res).toContain('"onClick":') + expect(res).toContain('function') + }) }) From cfda41cbf8d37474a5fb2d4d0b349997f9f4c590 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:30:09 +0900 Subject: [PATCH 47/51] Add case --- .../utils/__tests__/node-proxy.test.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index fc1ace4..9daccb8 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -606,4 +606,114 @@ describe('nodeProxyTracker', () => { const mainComponent = await getMainComponentAsync() expect(mainComponent).toBeNull() }) + + test('toTestCaseFormatWithVariables should collect variable info from fills', async () => { + // Setup figma global mock + const mockFigma = { + variables: { + getVariableByIdAsync: async (id: string) => { + if (id === 'VariableID:123') { + return { id: 'VariableID:123', name: 'primary-color' } + } + return null + }, + }, + } + ;(globalThis as unknown as { figma: typeof mockFigma }).figma = mockFigma + + // Create a node with boundVariables referencing a variable + const nodeWithVariable = { + ...createMockNode({ id: 'node-1', name: 'NodeWithVariable' }), + fills: [ + { + type: 'SOLID', + color: { r: 1, g: 0, b: 0 }, + boundVariables: { + color: '[NodeId: VariableID:123]', + }, + }, + ], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithVariable) + + // Access fills to trigger tracking + const _fills = (wrapped as unknown as FrameNode).fills + + // Call toTestCaseFormatWithVariables + const result = await nodeProxyTracker.toTestCaseFormatWithVariables() + + expect(result.nodes.length).toBe(1) + expect(result.variables.length).toBe(1) + expect(result.variables[0].id).toBe('VariableID:123') + expect(result.variables[0].name).toBe('primary-color') + }) + + test('assembleNodeTree should setup variable mocks when variables provided', async () => { + // Create nodes with variable reference in fills + const nodes = [ + { + id: 'node-1', + name: 'NodeWithVariable', + type: 'FRAME', + fills: [ + { + type: 'SOLID', + boundVariables: { + color: 'VariableID:456', + }, + }, + ], + }, + ] + + const variables = [{ id: 'VariableID:456', name: 'secondary-color' }] + + // Call assembleNodeTree with variables + const rootNode = assembleNodeTree(nodes, variables) + + expect(rootNode.id).toBe('node-1') + + // Verify variable mock was set up + const g = globalThis as { + figma?: { + variables?: { getVariableByIdAsync?: (id: string) => Promise } + } + } + expect(g.figma?.variables?.getVariableByIdAsync).toBeDefined() + + // Call the mock to verify it returns the variable info + const getVariableByIdAsync = g.figma?.variables?.getVariableByIdAsync + if (getVariableByIdAsync) { + const variable = await getVariableByIdAsync('VariableID:456') + expect(variable).toEqual({ + id: 'VariableID:456', + name: 'secondary-color', + }) + } + }) + + test('assembleNodeTree should add getMainComponentAsync to non-INSTANCE nodes', async () => { + // Create a FRAME node (not INSTANCE) + const nodes = [ + { + id: 'frame-1', + name: 'FrameNode', + type: 'FRAME', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('FRAME') + + // The FRAME node should have getMainComponentAsync method (default fallback) + expect(typeof rootNode.getMainComponentAsync).toBe('function') + + // Call the method and verify it returns null + const getMainComponentAsync = + rootNode.getMainComponentAsync as () => Promise + const result = await getMainComponentAsync() + expect(result).toBeNull() + }) }) From d25d15232ddc7005e094fb6c2e38ce5710a5b634 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:41:28 +0900 Subject: [PATCH 48/51] Add case --- .../utils/__tests__/node-proxy.test.ts | 146 ++++++++++++++++++ src/codegen/utils/node-proxy.ts | 52 +++++-- 2 files changed, 184 insertions(+), 14 deletions(-) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index 9daccb8..3363e2e 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -716,4 +716,150 @@ describe('nodeProxyTracker', () => { const result = await getMainComponentAsync() expect(result).toBeNull() }) + + test('assembleNodeTree should link children by id references', () => { + // Create nodes with children as string IDs (how test data comes from toTestCaseFormat) + const nodes = [ + { + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + children: ['child-1', 'child-2'], + }, + { + id: 'child-1', + name: 'Child1', + type: 'FRAME', + }, + { + id: 'child-2', + name: 'Child2', + type: 'RECTANGLE', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.id).toBe('parent-1') + expect(Array.isArray(rootNode.children)).toBe(true) + expect(rootNode.children?.length).toBe(2) + + // Children should be linked as objects, not strings + const child1 = rootNode.children?.[0] + expect(typeof child1).toBe('object') + expect((child1 as { id: string })?.id).toBe('child-1') + + const child2 = rootNode.children?.[1] + expect(typeof child2).toBe('object') + expect((child2 as { id: string })?.id).toBe('child-2') + }) + + test('assembleNodeTree should filter out undefined children', () => { + // Create nodes with children referencing non-existent nodes + const nodes = [ + { + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + children: ['child-1', 'non-existent-child'], + }, + { + id: 'child-1', + name: 'Child1', + type: 'FRAME', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.id).toBe('parent-1') + expect(Array.isArray(rootNode.children)).toBe(true) + // Only child-1 should be in children, non-existent-child should be filtered out + expect(rootNode.children?.length).toBe(1) + expect((rootNode.children?.[0] as { id: string })?.id).toBe('child-1') + }) + + test('assembleNodeTree TEXT node getStyledTextSegments should return stored segments', () => { + // Create TEXT node with styledTextSegments data + const nodes = [ + { + id: 'text-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Hello World', + styledTextSegments: [ + { + start: 0, + end: 5, + characters: 'Hello', + fontName: { family: 'Arial', style: 'Bold' }, + fontWeight: 700, + fontSize: 20, + }, + { + start: 6, + end: 11, + characters: 'World', + fontName: { family: 'Arial', style: 'Regular' }, + fontWeight: 400, + fontSize: 16, + }, + ], + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('TEXT') + + // Call getStyledTextSegments + const getStyledTextSegments = ( + rootNode as unknown as Record + ).getStyledTextSegments as () => unknown[] + expect(typeof getStyledTextSegments).toBe('function') + + const segments = getStyledTextSegments() + expect(Array.isArray(segments)).toBe(true) + expect(segments.length).toBe(2) + expect((segments[0] as { characters: string }).characters).toBe('Hello') + expect((segments[1] as { characters: string }).characters).toBe('World') + }) + + test('assembleNodeTree TEXT node getStyledTextSegments should generate default segment when no styledTextSegments', () => { + // Create TEXT node without styledTextSegments + const nodes = [ + { + id: 'text-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Test Text', + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + fontSize: 14, + lineHeight: { unit: 'AUTO' }, + letterSpacing: { unit: 'PERCENT', value: 0 }, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }], + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('TEXT') + + // Call getStyledTextSegments - should generate default segment + const getStyledTextSegments = ( + rootNode as unknown as Record + ).getStyledTextSegments as () => unknown[] + const segments = getStyledTextSegments() + + expect(Array.isArray(segments)).toBe(true) + expect(segments.length).toBe(1) + + const segment = segments[0] as Record + expect(segment.characters).toBe('Test Text') + expect(segment.start).toBe(0) + expect(segment.end).toBe(9) + expect(segment.textDecoration).toBe('NONE') + expect(segment.textCase).toBe('ORIGINAL') + }) }) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index ac77f86..a0e2692 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -225,10 +225,12 @@ class NodeProxyTracker { >)[], ) // 세그먼트 직렬화 - const serializedSegments = segments.map((seg) => ({ - ...seg, - fills: this.serializeArray(seg.fills as unknown[]), - })) + const serializedSegments = segments.map((seg) => { + return { + ...seg, + fills: this.serializeArray(seg.fills as unknown[]), + } + }) const log = this.accessLogs.get(node.id) if (log) { @@ -264,14 +266,20 @@ class NodeProxyTracker { private isArrayLikeObject(obj: Record): boolean { const keys = Object.keys(obj) if (keys.length === 0) return false - return keys.every((key) => /^\d+$/.test(key)) + return keys.every((key) => { + return /^\d+$/.test(key) + }) } private arrayLikeToArray(obj: Record): unknown[] { const keys = Object.keys(obj) .map(Number) - .sort((a, b) => a - b) - return keys.map((key) => obj[key]) + .sort((a, b) => { + return a - b + }) + return keys.map((key) => { + return obj[key] + }) } private serializeObject( @@ -414,7 +422,9 @@ class NodeProxyTracker { return allNodes } - const rootNode = allNodes.find((n) => n.id === rootId) + const rootNode = allNodes.find((n) => { + return n.id === rootId + }) if (!rootNode) { return allNodes } @@ -422,7 +432,9 @@ class NodeProxyTracker { // 하위 노드 ID들을 수집 (children을 재귀적으로 탐색) const descendantIds = new Set() const collectDescendants = (nodeId: string) => { - const node = allNodes.find((n) => n.id === nodeId) + const node = allNodes.find((n) => { + return n.id === nodeId + }) if (!node) return if (Array.isArray(node.children)) { for (const childId of node.children) { @@ -447,7 +459,9 @@ class NodeProxyTracker { const parentId = typeof rootNode.parent === 'string' ? rootNode.parent : undefined if (parentId) { - const parentNode = allNodes.find((n) => n.id === parentId) + const parentNode = allNodes.find((n) => { + return n.id === parentId + }) if (parentNode && parentNode.type === 'SECTION') { parentNode.children = [rootId] result.push(parentNode) @@ -465,7 +479,9 @@ class NodeProxyTracker { } } if (Array.isArray(value)) { - return value.map((item) => this.resolveNodeRefs(item)) + return value.map((item) => { + return this.resolveNodeRefs(item) + }) } return value } @@ -519,7 +535,11 @@ function setupVariableMocks(variables: VariableInfo[]): void { const g = globalThis as { figma?: { variables?: Record } } if (!g.figma) return - const variableMap = new Map(variables.map((v) => [v.id, v])) + const variableMap = new Map( + variables.map((v) => { + return [v.id, v] + }), + ) // 기존 mock을 보존하면서 새로운 변수들 추가 const originalGetVariable = g.figma.variables?.getVariableByIdAsync as @@ -579,7 +599,9 @@ export function assembleNodeTree( } return childId }) - .filter((child): child is NodeData => child !== undefined) + .filter((child): child is NodeData => { + return child !== undefined + }) } // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) @@ -668,7 +690,9 @@ export function assembleNodeTree( // 모든 노드에 getMainComponentAsync 메서드 추가 (아직 없는 경우에만) if (!(node as unknown as Record).getMainComponentAsync) { ;(node as unknown as Record).getMainComponentAsync = - async () => null + async () => { + return null + } } // TEXT 노드에 getStyledTextSegments mock 메서드 추가 From 5d50b1e6c2706aec735a4ec721bbe88b2970b3e9 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:49:36 +0900 Subject: [PATCH 49/51] Split --- .../__tests__/assemble-node-tree.test.ts | 356 ++++++++++++++++++ .../utils/__tests__/node-proxy.test.ts | 242 +----------- src/codegen/utils/assemble-node-tree.ts | 227 +++++++++++ src/codegen/utils/node-proxy.ts | 229 +---------- 4 files changed, 587 insertions(+), 467 deletions(-) create mode 100644 src/codegen/utils/__tests__/assemble-node-tree.test.ts create mode 100644 src/codegen/utils/assemble-node-tree.ts diff --git a/src/codegen/utils/__tests__/assemble-node-tree.test.ts b/src/codegen/utils/__tests__/assemble-node-tree.test.ts new file mode 100644 index 0000000..c2c3890 --- /dev/null +++ b/src/codegen/utils/__tests__/assemble-node-tree.test.ts @@ -0,0 +1,356 @@ +import { describe, expect, test } from 'bun:test' +import { assembleNodeTree, setupVariableMocks } from '../assemble-node-tree' + +describe('assembleNodeTree', () => { + test('should add getMainComponentAsync mock to INSTANCE node', async () => { + // Create node data for an INSTANCE node + const nodes = [ + { + id: 'instance-1', + name: 'InstanceNode', + type: 'INSTANCE', + mainComponent: null, + }, + ] + + // assembleNodeTree processes nodes and adds getMainComponentAsync to INSTANCE nodes + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('INSTANCE') + + // The prepared node should have getMainComponentAsync method + expect(typeof rootNode.getMainComponentAsync).toBe('function') + + // Call the method and verify it returns null + const getMainComponentAsync = + rootNode.getMainComponentAsync as () => Promise + const mainComponent = await getMainComponentAsync() + expect(mainComponent).toBeNull() + }) + + test('should setup variable mocks when variables provided', async () => { + // Setup globalThis.figma first (required for setupVariableMocks) + ;(globalThis as unknown as { figma: Record }).figma = {} + + // Create nodes with variable reference in fills + const nodes = [ + { + id: 'node-1', + name: 'NodeWithVariable', + type: 'FRAME', + fills: [ + { + type: 'SOLID', + boundVariables: { + color: 'VariableID:456', + }, + }, + ], + }, + ] + + const variables = [{ id: 'VariableID:456', name: 'secondary-color' }] + + // Call assembleNodeTree with variables + const rootNode = assembleNodeTree(nodes, variables) + + expect(rootNode.id).toBe('node-1') + + // Verify variable mock was set up + const g = globalThis as { + figma?: { + variables?: { getVariableByIdAsync?: (id: string) => Promise } + } + } + expect(g.figma?.variables?.getVariableByIdAsync).toBeDefined() + + // Call the mock to verify it returns the variable info + const getVariableByIdAsync = g.figma?.variables?.getVariableByIdAsync + if (getVariableByIdAsync) { + const variable = await getVariableByIdAsync('VariableID:456') + expect(variable).toEqual({ + id: 'VariableID:456', + name: 'secondary-color', + }) + } + }) + + test('should add getMainComponentAsync to non-INSTANCE nodes', async () => { + // Create a FRAME node (not INSTANCE) + const nodes = [ + { + id: 'frame-1', + name: 'FrameNode', + type: 'FRAME', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('FRAME') + + // The FRAME node should have getMainComponentAsync method (default fallback) + expect(typeof rootNode.getMainComponentAsync).toBe('function') + + // Call the method and verify it returns null + const getMainComponentAsync = + rootNode.getMainComponentAsync as () => Promise + const result = await getMainComponentAsync() + expect(result).toBeNull() + }) + + test('should link children by id references', () => { + // Create nodes with children as string IDs (how test data comes from toTestCaseFormat) + const nodes = [ + { + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + children: ['child-1', 'child-2'], + }, + { + id: 'child-1', + name: 'Child1', + type: 'FRAME', + }, + { + id: 'child-2', + name: 'Child2', + type: 'RECTANGLE', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.id).toBe('parent-1') + expect(Array.isArray(rootNode.children)).toBe(true) + expect(rootNode.children?.length).toBe(2) + + // Children should be linked as objects, not strings + const child1 = rootNode.children?.[0] + expect(typeof child1).toBe('object') + expect((child1 as { id: string })?.id).toBe('child-1') + + const child2 = rootNode.children?.[1] + expect(typeof child2).toBe('object') + expect((child2 as { id: string })?.id).toBe('child-2') + }) + + test('should filter out undefined children', () => { + // Create nodes with children referencing non-existent nodes + const nodes = [ + { + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + children: ['child-1', 'non-existent-child'], + }, + { + id: 'child-1', + name: 'Child1', + type: 'FRAME', + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.id).toBe('parent-1') + expect(Array.isArray(rootNode.children)).toBe(true) + // Only child-1 should be in children, non-existent-child should be filtered out + expect(rootNode.children?.length).toBe(1) + expect((rootNode.children?.[0] as { id: string })?.id).toBe('child-1') + }) + + test('TEXT node getStyledTextSegments should return stored segments', () => { + // Create TEXT node with styledTextSegments data + const nodes = [ + { + id: 'text-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Hello World', + styledTextSegments: [ + { + start: 0, + end: 5, + characters: 'Hello', + fontName: { family: 'Arial', style: 'Bold' }, + fontWeight: 700, + fontSize: 20, + }, + { + start: 6, + end: 11, + characters: 'World', + fontName: { family: 'Arial', style: 'Regular' }, + fontWeight: 400, + fontSize: 16, + }, + ], + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('TEXT') + + // Call getStyledTextSegments + const getStyledTextSegments = ( + rootNode as unknown as Record + ).getStyledTextSegments as () => unknown[] + expect(typeof getStyledTextSegments).toBe('function') + + const segments = getStyledTextSegments() + expect(Array.isArray(segments)).toBe(true) + expect(segments.length).toBe(2) + expect((segments[0] as { characters: string }).characters).toBe('Hello') + expect((segments[1] as { characters: string }).characters).toBe('World') + }) + + test('TEXT node getStyledTextSegments should generate default segment when no styledTextSegments', () => { + // Create TEXT node without styledTextSegments + const nodes = [ + { + id: 'text-1', + name: 'TextNode', + type: 'TEXT', + characters: 'Test Text', + fontName: { family: 'Inter', style: 'Regular' }, + fontWeight: 400, + fontSize: 14, + lineHeight: { unit: 'AUTO' }, + letterSpacing: { unit: 'PERCENT', value: 0 }, + fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }], + }, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.type).toBe('TEXT') + + // Call getStyledTextSegments - should generate default segment + const getStyledTextSegments = ( + rootNode as unknown as Record + ).getStyledTextSegments as () => unknown[] + const segments = getStyledTextSegments() + + expect(Array.isArray(segments)).toBe(true) + expect(segments.length).toBe(1) + + const segment = segments[0] as Record + expect(segment.characters).toBe('Test Text') + expect(segment.start).toBe(0) + expect(segment.end).toBe(9) + expect(segment.textDecoration).toBe('NONE') + expect(segment.textCase).toBe('ORIGINAL') + }) + + test('should handle children that are already NodeData objects', () => { + // Create nodes where children are already objects, not string IDs + const childNode = { + id: 'child-1', + name: 'Child1', + type: 'FRAME', + } + + const nodes = [ + { + id: 'parent-1', + name: 'Parent', + type: 'FRAME', + children: [childNode], // Already an object, not a string ID + }, + childNode, + ] + + const rootNode = assembleNodeTree(nodes) + + expect(rootNode.id).toBe('parent-1') + expect(Array.isArray(rootNode.children)).toBe(true) + expect(rootNode.children?.length).toBe(1) + // Child should still be an object + expect((rootNode.children?.[0] as { id: string })?.id).toBe('child-1') + }) +}) + +describe('setupVariableMocks', () => { + test('should setup figma.variables.getVariableByIdAsync mock', async () => { + // Setup globalThis.figma + ;(globalThis as unknown as { figma: Record }).figma = {} + + const variables = [ + { id: 'VariableID:123', name: 'test-color' }, + { id: 'VariableID:456', name: 'another-color' }, + ] + + setupVariableMocks(variables) + + const g = globalThis as { + figma?: { + variables?: { getVariableByIdAsync?: (id: string) => Promise } + } + } + + expect(g.figma?.variables?.getVariableByIdAsync).toBeDefined() + + // Test the mock returns correct values + if (g.figma?.variables?.getVariableByIdAsync) { + const result1 = + await g.figma.variables.getVariableByIdAsync('VariableID:123') + expect(result1).toEqual({ id: 'VariableID:123', name: 'test-color' }) + + const result2 = + await g.figma.variables.getVariableByIdAsync('VariableID:456') + expect(result2).toEqual({ id: 'VariableID:456', name: 'another-color' }) + + // Non-existent variable should return null + const result3 = + await g.figma.variables.getVariableByIdAsync('VariableID:789') + expect(result3).toBeNull() + } + }) + + test('should fallback to original mock when variable not found', async () => { + // Setup globalThis.figma with an existing mock + const originalMock = async (id: string) => { + if (id === 'VariableID:original') { + return { id: 'VariableID:original', name: 'original-color' } + } + return null + } + + ;( + globalThis as unknown as { + figma: { variables: { getVariableByIdAsync: typeof originalMock } } + } + ).figma = { + variables: { + getVariableByIdAsync: originalMock, + }, + } + + // Now call setupVariableMocks with new variables + const variables = [{ id: 'VariableID:new', name: 'new-color' }] + setupVariableMocks(variables) + + const g = globalThis as { + figma?: { + variables?: { getVariableByIdAsync?: (id: string) => Promise } + } + } + + // New variable should work + const result1 = + await g.figma?.variables?.getVariableByIdAsync?.('VariableID:new') + expect(result1).toEqual({ id: 'VariableID:new', name: 'new-color' }) + + // Original mock should still work via fallback + const result2 = await g.figma?.variables?.getVariableByIdAsync?.( + 'VariableID:original', + ) + expect(result2).toEqual({ + id: 'VariableID:original', + name: 'original-color', + }) + }) +}) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index 3363e2e..29cb18b 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'bun:test' -import { assembleNodeTree, nodeProxyTracker } from '../node-proxy' +import { nodeProxyTracker } from '../node-proxy' // Mock SceneNode function createMockNode(overrides: Partial = {}): SceneNode { @@ -581,32 +581,6 @@ describe('nodeProxyTracker', () => { expect(fills[0]).toEqual(['value0', 'value1', 'value2']) }) - test('should add getMainComponentAsync mock to INSTANCE node via assembleNodeTree', async () => { - // Create node data for an INSTANCE node - const nodes = [ - { - id: 'instance-1', - name: 'InstanceNode', - type: 'INSTANCE', - mainComponent: null, - }, - ] - - // assembleNodeTree processes nodes and adds getMainComponentAsync to INSTANCE nodes - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.type).toBe('INSTANCE') - - // The prepared node should have getMainComponentAsync method - expect(typeof rootNode.getMainComponentAsync).toBe('function') - - // Call the method and verify it returns null - const getMainComponentAsync = - rootNode.getMainComponentAsync as () => Promise - const mainComponent = await getMainComponentAsync() - expect(mainComponent).toBeNull() - }) - test('toTestCaseFormatWithVariables should collect variable info from fills', async () => { // Setup figma global mock const mockFigma = { @@ -648,218 +622,4 @@ describe('nodeProxyTracker', () => { expect(result.variables[0].id).toBe('VariableID:123') expect(result.variables[0].name).toBe('primary-color') }) - - test('assembleNodeTree should setup variable mocks when variables provided', async () => { - // Create nodes with variable reference in fills - const nodes = [ - { - id: 'node-1', - name: 'NodeWithVariable', - type: 'FRAME', - fills: [ - { - type: 'SOLID', - boundVariables: { - color: 'VariableID:456', - }, - }, - ], - }, - ] - - const variables = [{ id: 'VariableID:456', name: 'secondary-color' }] - - // Call assembleNodeTree with variables - const rootNode = assembleNodeTree(nodes, variables) - - expect(rootNode.id).toBe('node-1') - - // Verify variable mock was set up - const g = globalThis as { - figma?: { - variables?: { getVariableByIdAsync?: (id: string) => Promise } - } - } - expect(g.figma?.variables?.getVariableByIdAsync).toBeDefined() - - // Call the mock to verify it returns the variable info - const getVariableByIdAsync = g.figma?.variables?.getVariableByIdAsync - if (getVariableByIdAsync) { - const variable = await getVariableByIdAsync('VariableID:456') - expect(variable).toEqual({ - id: 'VariableID:456', - name: 'secondary-color', - }) - } - }) - - test('assembleNodeTree should add getMainComponentAsync to non-INSTANCE nodes', async () => { - // Create a FRAME node (not INSTANCE) - const nodes = [ - { - id: 'frame-1', - name: 'FrameNode', - type: 'FRAME', - }, - ] - - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.type).toBe('FRAME') - - // The FRAME node should have getMainComponentAsync method (default fallback) - expect(typeof rootNode.getMainComponentAsync).toBe('function') - - // Call the method and verify it returns null - const getMainComponentAsync = - rootNode.getMainComponentAsync as () => Promise - const result = await getMainComponentAsync() - expect(result).toBeNull() - }) - - test('assembleNodeTree should link children by id references', () => { - // Create nodes with children as string IDs (how test data comes from toTestCaseFormat) - const nodes = [ - { - id: 'parent-1', - name: 'Parent', - type: 'FRAME', - children: ['child-1', 'child-2'], - }, - { - id: 'child-1', - name: 'Child1', - type: 'FRAME', - }, - { - id: 'child-2', - name: 'Child2', - type: 'RECTANGLE', - }, - ] - - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.id).toBe('parent-1') - expect(Array.isArray(rootNode.children)).toBe(true) - expect(rootNode.children?.length).toBe(2) - - // Children should be linked as objects, not strings - const child1 = rootNode.children?.[0] - expect(typeof child1).toBe('object') - expect((child1 as { id: string })?.id).toBe('child-1') - - const child2 = rootNode.children?.[1] - expect(typeof child2).toBe('object') - expect((child2 as { id: string })?.id).toBe('child-2') - }) - - test('assembleNodeTree should filter out undefined children', () => { - // Create nodes with children referencing non-existent nodes - const nodes = [ - { - id: 'parent-1', - name: 'Parent', - type: 'FRAME', - children: ['child-1', 'non-existent-child'], - }, - { - id: 'child-1', - name: 'Child1', - type: 'FRAME', - }, - ] - - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.id).toBe('parent-1') - expect(Array.isArray(rootNode.children)).toBe(true) - // Only child-1 should be in children, non-existent-child should be filtered out - expect(rootNode.children?.length).toBe(1) - expect((rootNode.children?.[0] as { id: string })?.id).toBe('child-1') - }) - - test('assembleNodeTree TEXT node getStyledTextSegments should return stored segments', () => { - // Create TEXT node with styledTextSegments data - const nodes = [ - { - id: 'text-1', - name: 'TextNode', - type: 'TEXT', - characters: 'Hello World', - styledTextSegments: [ - { - start: 0, - end: 5, - characters: 'Hello', - fontName: { family: 'Arial', style: 'Bold' }, - fontWeight: 700, - fontSize: 20, - }, - { - start: 6, - end: 11, - characters: 'World', - fontName: { family: 'Arial', style: 'Regular' }, - fontWeight: 400, - fontSize: 16, - }, - ], - }, - ] - - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.type).toBe('TEXT') - - // Call getStyledTextSegments - const getStyledTextSegments = ( - rootNode as unknown as Record - ).getStyledTextSegments as () => unknown[] - expect(typeof getStyledTextSegments).toBe('function') - - const segments = getStyledTextSegments() - expect(Array.isArray(segments)).toBe(true) - expect(segments.length).toBe(2) - expect((segments[0] as { characters: string }).characters).toBe('Hello') - expect((segments[1] as { characters: string }).characters).toBe('World') - }) - - test('assembleNodeTree TEXT node getStyledTextSegments should generate default segment when no styledTextSegments', () => { - // Create TEXT node without styledTextSegments - const nodes = [ - { - id: 'text-1', - name: 'TextNode', - type: 'TEXT', - characters: 'Test Text', - fontName: { family: 'Inter', style: 'Regular' }, - fontWeight: 400, - fontSize: 14, - lineHeight: { unit: 'AUTO' }, - letterSpacing: { unit: 'PERCENT', value: 0 }, - fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }], - }, - ] - - const rootNode = assembleNodeTree(nodes) - - expect(rootNode.type).toBe('TEXT') - - // Call getStyledTextSegments - should generate default segment - const getStyledTextSegments = ( - rootNode as unknown as Record - ).getStyledTextSegments as () => unknown[] - const segments = getStyledTextSegments() - - expect(Array.isArray(segments)).toBe(true) - expect(segments.length).toBe(1) - - const segment = segments[0] as Record - expect(segment.characters).toBe('Test Text') - expect(segment.start).toBe(0) - expect(segment.end).toBe(9) - expect(segment.textDecoration).toBe('NONE') - expect(segment.textCase).toBe('ORIGINAL') - }) }) diff --git a/src/codegen/utils/assemble-node-tree.ts b/src/codegen/utils/assemble-node-tree.ts new file mode 100644 index 0000000..0373984 --- /dev/null +++ b/src/codegen/utils/assemble-node-tree.ts @@ -0,0 +1,227 @@ +import type { NodeData, VariableInfo } from './node-proxy' + +/** + * Sets up figma.variables.getVariableByIdAsync mock for test environment. + */ +export function setupVariableMocks(variables: VariableInfo[]): void { + if (typeof globalThis === 'undefined') return + + const g = globalThis as { figma?: { variables?: Record } } + if (!g.figma) return + + const variableMap = new Map( + variables.map((v) => { + return [v.id, v] + }), + ) + + // 기존 mock을 보존하면서 새로운 변수들 추가 + const originalGetVariable = g.figma.variables?.getVariableByIdAsync as + | ((id: string) => Promise) + | undefined + + g.figma.variables = { + ...g.figma.variables, + getVariableByIdAsync: async (id: string) => { + const varInfo = variableMap.get(id) + if (varInfo) { + return { id: varInfo.id, name: varInfo.name } + } + // 기존 mock이 있으면 폴백 + if (originalGetVariable) { + return originalGetVariable(id) + } + return null + }, + } +} + +/** + * Assembles a node tree from a flat list of nodes. + * Links parent/children by id references. + * Optionally sets up variable mocks from the variables array. + */ +export function assembleNodeTree( + nodes: NodeData[], + variables?: VariableInfo[], +): NodeData { + // Variable mock 설정 + if (variables && variables.length > 0) { + setupVariableMocks(variables) + } + const nodeMap = new Map() + + // 1. 모든 노드를 복사해서 맵에 저장 + for (const node of nodes) { + nodeMap.set(node.id, { ...node }) + } + + // 2. parent/children 관계 연결 및 TEXT 노드 mock 메서드 추가 + for (const node of nodeMap.values()) { + // parent 연결 + if (typeof node.parent === 'string') { + const parentNode = nodeMap.get(node.parent) + node.parent = parentNode + } + + // children 연결 (없는 노드는 필터링) + if (Array.isArray(node.children)) { + node.children = node.children + .map((childId) => { + if (typeof childId === 'string') { + return nodeMap.get(childId) + } + return childId + }) + .filter((child): child is NodeData => { + return child !== undefined + }) + } + // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) + + // defaultVariant 연결 (COMPONENT_SET용) + if (typeof node.defaultVariant === 'string') { + const defaultVariantNode = nodeMap.get(node.defaultVariant) + if (defaultVariantNode) { + node.defaultVariant = defaultVariantNode + } + } + + // fills/strokes의 boundVariables 처리 + // boundVariables.color가 문자열 ID인 경우 { id: '...' } 객체로 변환 + const processBoundVariables = (paints: unknown[]) => { + for (const paint of paints) { + if (paint && typeof paint === 'object') { + const p = paint as Record + if (p.boundVariables && typeof p.boundVariables === 'object') { + const bv = p.boundVariables as Record + if (typeof bv.color === 'string') { + // '[NodeId: VariableID:...]' 형식에서 실제 ID 추출 + let colorId = bv.color + const match = colorId.match(/\[NodeId: ([^\]]+)\]/) + if (match) { + colorId = match[1] + } + bv.color = { id: colorId } + } + } + + // Gradient stops의 boundVariables 처리 + if (Array.isArray(p.gradientStops)) { + for (const stop of p.gradientStops) { + if (stop && typeof stop === 'object') { + const s = stop as Record + if (s.boundVariables && typeof s.boundVariables === 'object') { + const bv = s.boundVariables as Record + if (typeof bv.color === 'string') { + let colorId = bv.color + const match = colorId.match(/\[NodeId: ([^\]]+)\]/) + if (match) { + colorId = match[1] + } + bv.color = { id: colorId } + } + } + } + } + } + } + } + } + if (Array.isArray(node.fills)) { + processBoundVariables(node.fills) + } + if (Array.isArray(node.strokes)) { + processBoundVariables(node.strokes) + } + + // strokeWeight가 없고 개별 stroke weights가 있으면 strokeWeight를 figma.mixed로 설정 + if ( + node.strokeWeight === undefined && + (node.strokeTopWeight !== undefined || + node.strokeBottomWeight !== undefined || + node.strokeLeftWeight !== undefined || + node.strokeRightWeight !== undefined) + ) { + const figmaGlobal = globalThis as unknown as { + figma?: { mixed?: unknown } + } + if (figmaGlobal.figma?.mixed) { + ;(node as unknown as Record).strokeWeight = + figmaGlobal.figma.mixed + } + } + + // INSTANCE 노드에 getMainComponentAsync mock 메서드 추가 + if (node.type === 'INSTANCE') { + ;(node as unknown as Record).getMainComponentAsync = + async () => { + // INSTANCE 노드는 mainComponent 속성을 참조하거나 null 반환 + return null + } + } + + // 모든 노드에 getMainComponentAsync 메서드 추가 (아직 없는 경우에만) + if (!(node as unknown as Record).getMainComponentAsync) { + ;(node as unknown as Record).getMainComponentAsync = + async () => { + return null + } + } + + // TEXT 노드에 getStyledTextSegments mock 메서드 추가 + if (node.type === 'TEXT') { + const textNode = node as NodeData & { styledTextSegments?: unknown[] } + + // styledTextSegments 내의 fills도 boundVariables 처리 + if (textNode.styledTextSegments) { + for (const seg of textNode.styledTextSegments) { + if (seg && typeof seg === 'object') { + const s = seg as Record + if (Array.isArray(s.fills)) { + processBoundVariables(s.fills) + } + } + } + } + + ;(node as unknown as Record).getStyledTextSegments = + () => { + // 테스트 데이터에 styledTextSegments가 있으면 사용 + if (textNode.styledTextSegments) { + return textNode.styledTextSegments + } + // 없으면 기본 세그먼트 생성 + return [ + { + characters: (node.characters as string) || '', + start: 0, + end: ((node.characters as string) || '').length, + fontName: (node.fontName as { family: string }) || { + family: 'Inter', + style: 'Regular', + }, + fontWeight: (node.fontWeight as number) || 400, + fontSize: (node.fontSize as number) || 12, + textDecoration: 'NONE', + textCase: 'ORIGINAL', + lineHeight: (node.lineHeight as unknown) || { unit: 'AUTO' }, + letterSpacing: (node.letterSpacing as unknown) || { + unit: 'PERCENT', + value: 0, + }, + fills: (node.fills as unknown[]) || [], + textStyleId: '', + fillStyleId: '', + listOptions: { type: 'NONE' }, + indentation: 0, + hyperlink: null, + }, + ] + } + } + } + + // 3. 첫 번째 노드(루트) 반환 + return nodeMap.get(nodes[0].id) || nodes[0] +} diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index a0e2692..674bcd3 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -526,231 +526,8 @@ export type TestCaseData = { variables: VariableInfo[] } -/** - * Sets up figma.variables.getVariableByIdAsync mock for test environment. - */ -function setupVariableMocks(variables: VariableInfo[]): void { - if (typeof globalThis === 'undefined') return - - const g = globalThis as { figma?: { variables?: Record } } - if (!g.figma) return - - const variableMap = new Map( - variables.map((v) => { - return [v.id, v] - }), - ) - - // 기존 mock을 보존하면서 새로운 변수들 추가 - const originalGetVariable = g.figma.variables?.getVariableByIdAsync as - | ((id: string) => Promise) - | undefined - - g.figma.variables = { - ...g.figma.variables, - getVariableByIdAsync: async (id: string) => { - const varInfo = variableMap.get(id) - if (varInfo) { - return { id: varInfo.id, name: varInfo.name } - } - // 기존 mock이 있으면 폴백 - if (originalGetVariable) { - return originalGetVariable(id) - } - return null - }, - } -} - -/** - * Assembles a node tree from a flat list of nodes. - * Links parent/children by id references. - * Optionally sets up variable mocks from the variables array. - */ -export function assembleNodeTree( - nodes: NodeData[], - variables?: VariableInfo[], -): NodeData { - // Variable mock 설정 - if (variables && variables.length > 0) { - setupVariableMocks(variables) - } - const nodeMap = new Map() - - // 1. 모든 노드를 복사해서 맵에 저장 - for (const node of nodes) { - nodeMap.set(node.id, { ...node }) - } - - // 2. parent/children 관계 연결 및 TEXT 노드 mock 메서드 추가 - for (const node of nodeMap.values()) { - // parent 연결 - if (typeof node.parent === 'string') { - const parentNode = nodeMap.get(node.parent) - node.parent = parentNode - } - - // children 연결 (없는 노드는 필터링) - if (Array.isArray(node.children)) { - node.children = node.children - .map((childId) => { - if (typeof childId === 'string') { - return nodeMap.get(childId) - } - return childId - }) - .filter((child): child is NodeData => { - return child !== undefined - }) - } - // children이 undefined인 경우 그대로 유지 (RECTANGLE 등 원래 children이 없는 노드) - - // defaultVariant 연결 (COMPONENT_SET용) - if (typeof node.defaultVariant === 'string') { - const defaultVariantNode = nodeMap.get(node.defaultVariant) - if (defaultVariantNode) { - node.defaultVariant = defaultVariantNode - } - } - - // fills/strokes의 boundVariables 처리 - // boundVariables.color가 문자열 ID인 경우 { id: '...' } 객체로 변환 - const processBoundVariables = (paints: unknown[]) => { - for (const paint of paints) { - if (paint && typeof paint === 'object') { - const p = paint as Record - if (p.boundVariables && typeof p.boundVariables === 'object') { - const bv = p.boundVariables as Record - if (typeof bv.color === 'string') { - // '[NodeId: VariableID:...]' 형식에서 실제 ID 추출 - let colorId = bv.color - const match = colorId.match(/\[NodeId: ([^\]]+)\]/) - if (match) { - colorId = match[1] - } - bv.color = { id: colorId } - } - } - - // Gradient stops의 boundVariables 처리 - if (Array.isArray(p.gradientStops)) { - for (const stop of p.gradientStops) { - if (stop && typeof stop === 'object') { - const s = stop as Record - if (s.boundVariables && typeof s.boundVariables === 'object') { - const bv = s.boundVariables as Record - if (typeof bv.color === 'string') { - let colorId = bv.color - const match = colorId.match(/\[NodeId: ([^\]]+)\]/) - if (match) { - colorId = match[1] - } - bv.color = { id: colorId } - } - } - } - } - } - } - } - } - if (Array.isArray(node.fills)) { - processBoundVariables(node.fills) - } - if (Array.isArray(node.strokes)) { - processBoundVariables(node.strokes) - } - - // strokeWeight가 없고 개별 stroke weights가 있으면 strokeWeight를 figma.mixed로 설정 - if ( - node.strokeWeight === undefined && - (node.strokeTopWeight !== undefined || - node.strokeBottomWeight !== undefined || - node.strokeLeftWeight !== undefined || - node.strokeRightWeight !== undefined) - ) { - const figmaGlobal = globalThis as unknown as { - figma?: { mixed?: unknown } - } - if (figmaGlobal.figma?.mixed) { - ;(node as unknown as Record).strokeWeight = - figmaGlobal.figma.mixed - } - } - - // INSTANCE 노드에 getMainComponentAsync mock 메서드 추가 - if (node.type === 'INSTANCE') { - ;(node as unknown as Record).getMainComponentAsync = - async () => { - // INSTANCE 노드는 mainComponent 속성을 참조하거나 null 반환 - return null - } - } - - // 모든 노드에 getMainComponentAsync 메서드 추가 (아직 없는 경우에만) - if (!(node as unknown as Record).getMainComponentAsync) { - ;(node as unknown as Record).getMainComponentAsync = - async () => { - return null - } - } - - // TEXT 노드에 getStyledTextSegments mock 메서드 추가 - if (node.type === 'TEXT') { - const textNode = node as NodeData & { styledTextSegments?: unknown[] } - - // styledTextSegments 내의 fills도 boundVariables 처리 - if (textNode.styledTextSegments) { - for (const seg of textNode.styledTextSegments) { - if (seg && typeof seg === 'object') { - const s = seg as Record - if (Array.isArray(s.fills)) { - processBoundVariables(s.fills) - } - } - } - } - - ;(node as unknown as Record).getStyledTextSegments = - () => { - // 테스트 데이터에 styledTextSegments가 있으면 사용 - if (textNode.styledTextSegments) { - return textNode.styledTextSegments - } - // 없으면 기본 세그먼트 생성 - return [ - { - characters: (node.characters as string) || '', - start: 0, - end: ((node.characters as string) || '').length, - fontName: (node.fontName as { family: string }) || { - family: 'Inter', - style: 'Regular', - }, - fontWeight: (node.fontWeight as number) || 400, - fontSize: (node.fontSize as number) || 12, - textDecoration: 'NONE', - textCase: 'ORIGINAL', - lineHeight: (node.lineHeight as unknown) || { unit: 'AUTO' }, - letterSpacing: (node.letterSpacing as unknown) || { - unit: 'PERCENT', - value: 0, - }, - fills: (node.fills as unknown[]) || [], - textStyleId: '', - fillStyleId: '', - listOptions: { type: 'NONE' }, - indentation: 0, - hyperlink: null, - }, - ] - } - } - } - - // 3. 첫 번째 노드(루트) 반환 - return nodeMap.get(nodes[0].id) || nodes[0] -} - // 싱글톤 인스턴스 export const nodeProxyTracker = new NodeProxyTracker() + +// Re-export from assemble-node-tree for backwards compatibility +export { assembleNodeTree, setupVariableMocks } from './assemble-node-tree' From 69e2084d053b74c5bbea5e706346829619104cf4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:54:19 +0900 Subject: [PATCH 50/51] Add case --- .../utils/__tests__/node-proxy.test.ts | 148 +++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/src/codegen/utils/__tests__/node-proxy.test.ts b/src/codegen/utils/__tests__/node-proxy.test.ts index 29cb18b..ab030b6 100644 --- a/src/codegen/utils/__tests__/node-proxy.test.ts +++ b/src/codegen/utils/__tests__/node-proxy.test.ts @@ -1,5 +1,9 @@ import { beforeEach, describe, expect, test } from 'bun:test' -import { nodeProxyTracker } from '../node-proxy' +import { + assembleNodeTree, + nodeProxyTracker, + setupVariableMocks, +} from '../node-proxy' // Mock SceneNode function createMockNode(overrides: Partial = {}): SceneNode { @@ -622,4 +626,146 @@ describe('nodeProxyTracker', () => { expect(result.variables[0].id).toBe('VariableID:123') expect(result.variables[0].name).toBe('primary-color') }) + + test('toTestCaseFormatWithVariables should collect variables from nested objects', async () => { + // Setup figma global mock + const mockFigma = { + variables: { + getVariableByIdAsync: async (id: string) => { + if (id === 'VariableID:nested') { + return { id: 'VariableID:nested', name: 'nested-color' } + } + return null + }, + }, + } + ;(globalThis as unknown as { figma: typeof mockFigma }).figma = mockFigma + + // Create a node with nested object containing variable reference + const nodeWithNestedVariable = { + ...createMockNode({ id: 'node-nested', name: 'NodeWithNestedVariable' }), + customProp: { + nested: { + deep: { + variableRef: '[NodeId: VariableID:nested]', + }, + }, + }, + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithNestedVariable) + + // Access customProp to trigger tracking + const _customProp = (wrapped as unknown as Record) + .customProp + + // Call toTestCaseFormatWithVariables + const result = await nodeProxyTracker.toTestCaseFormatWithVariables() + + expect(result.variables.length).toBe(1) + expect(result.variables[0].id).toBe('VariableID:nested') + expect(result.variables[0].name).toBe('nested-color') + }) + + test('toTestCaseFormatWithVariables should collect variables from arrays', async () => { + // Setup figma global mock + const mockFigma = { + variables: { + getVariableByIdAsync: async (id: string) => { + if (id === 'VariableID:arr1') { + return { id: 'VariableID:arr1', name: 'arr-color-1' } + } + if (id === 'VariableID:arr2') { + return { id: 'VariableID:arr2', name: 'arr-color-2' } + } + return null + }, + }, + } + ;(globalThis as unknown as { figma: typeof mockFigma }).figma = mockFigma + + // Create a node with array containing variable references + const nodeWithArrayVariable = { + ...createMockNode({ id: 'node-arr', name: 'NodeWithArrayVariable' }), + fills: [ + { + type: 'SOLID', + boundVariables: { + color: '[NodeId: VariableID:arr1]', + }, + }, + { + type: 'SOLID', + boundVariables: { + color: '[NodeId: VariableID:arr2]', + }, + }, + ], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithArrayVariable) + + // Access fills to trigger tracking + const _fills = (wrapped as unknown as FrameNode).fills + + // Call toTestCaseFormatWithVariables + const result = await nodeProxyTracker.toTestCaseFormatWithVariables() + + expect(result.variables.length).toBe(2) + const varIds = result.variables.map((v) => v.id) + expect(varIds).toContain('VariableID:arr1') + expect(varIds).toContain('VariableID:arr2') + }) + + test('toTestCaseFormatWithVariables should collect variables from direct array of variable refs', async () => { + // Setup figma global mock + const mockFigma = { + variables: { + getVariableByIdAsync: async (id: string) => { + if (id === 'VariableID:direct') { + return { id: 'VariableID:direct', name: 'direct-color' } + } + return null + }, + }, + } + ;(globalThis as unknown as { figma: typeof mockFigma }).figma = mockFigma + + // Create a node with direct array of variable reference strings + const nodeWithDirectArray = { + ...createMockNode({ id: 'node-direct', name: 'NodeWithDirectArray' }), + colorRefs: ['[NodeId: VariableID:direct]'], + } as unknown as SceneNode + + const wrapped = nodeProxyTracker.wrap(nodeWithDirectArray) + + // Access colorRefs to trigger tracking + const _colorRefs = (wrapped as unknown as Record).colorRefs + + // Call toTestCaseFormatWithVariables + const result = await nodeProxyTracker.toTestCaseFormatWithVariables() + + expect(result.variables.length).toBe(1) + expect(result.variables[0].id).toBe('VariableID:direct') + }) + + test('re-exported assembleNodeTree works from node-proxy', () => { + const nodes = [{ id: 'test-1', name: 'Test', type: 'FRAME' }] + const result = assembleNodeTree(nodes) + expect(result.id).toBe('test-1') + }) + + test('re-exported setupVariableMocks works from node-proxy', async () => { + ;(globalThis as unknown as { figma: Record }).figma = {} + setupVariableMocks([{ id: 'VariableID:test', name: 'test-var' }]) + + const g = globalThis as { + figma?: { + variables?: { getVariableByIdAsync?: (id: string) => Promise } + } + } + const result = + await g.figma?.variables?.getVariableByIdAsync?.('VariableID:test') + expect(result).toEqual({ id: 'VariableID:test', name: 'test-var' }) + }) }) From 4f110aac11c710e4cefd6b2ea3884ad0cd8bd2fd Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 21:12:26 +0900 Subject: [PATCH 51/51] Increase coverage --- src/codegen/utils/node-proxy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/codegen/utils/node-proxy.ts b/src/codegen/utils/node-proxy.ts index 674bcd3..e2c5977 100644 --- a/src/codegen/utils/node-proxy.ts +++ b/src/codegen/utils/node-proxy.ts @@ -13,6 +13,9 @@ type AccessLog = { class NodeProxyTracker { private accessLogs: Map = new Map() + // biome-ignore lint/complexity/noUselessConstructor: Required for bun test coverage tracking + constructor() {} + clear() { this.accessLogs.clear() }