diff --git a/CHANGELOG.md b/CHANGELOG.md index 986f8ef90..bda115f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Added +- Detach an instance from its component with `detachInstance()` in the plugin API, as in Figma, from scripts run through `eval`. - Run scripts written for Figma's dynamic-page mode that call `figma.getNodeByIdAsync()` or `getMainComponentAsync()`; both resolve to the same nodes as their synchronous forms. - Export components to Storybook with `openpencil export -f storybook`: one CSF3 story file per component set or component for React, Vue, or HTML, with a story and `select` controls per variant, a design image per variant, and an `openpencil://` link that opens the variant in OpenPencil. `--watch` re-exports on every save and removes stories of deleted components (#727). - Export HTML and Tailwind JSX from the app's export options and through the IO registry, and export Tailwind JSX from the CLI with `-f tailwind-jsx` (`-f jsx --style tailwind` still works). HTML export of a single layer now includes the layer itself, as other formats do. diff --git a/packages/core/src/figma-api/compatibility.ts b/packages/core/src/figma-api/compatibility.ts index dd6a8e356..e4f639b78 100644 --- a/packages/core/src/figma-api/compatibility.ts +++ b/packages/core/src/figma-api/compatibility.ts @@ -71,6 +71,7 @@ type InstancePropertySurfaceMatch = Expect< | 'isExposedInstance' | 'exposedInstances' | 'getMainComponentAsync' + | 'detachInstance' >, Pick< InstanceNode, @@ -80,6 +81,7 @@ type InstancePropertySurfaceMatch = Expect< | 'isExposedInstance' | 'exposedInstances' | 'getMainComponentAsync' + | 'detachInstance' > > > diff --git a/packages/core/src/figma-api/proxy.ts b/packages/core/src/figma-api/proxy.ts index 641760c65..e09f6d752 100644 --- a/packages/core/src/figma-api/proxy.ts +++ b/packages/core/src/figma-api/proxy.ts @@ -244,6 +244,15 @@ export class FigmaNodeProxy { return this[INTERNAL_API].wrapNode(inst.id) } + /** Turns this instance into a frame that keeps its current content, like Figma's. */ + detachInstance(): FigmaNodeProxy { + const n = this._raw() + if (n.type !== 'INSTANCE') throw new Error('detachInstance() can only be called on instances') + assertNodeEditable(this[INTERNAL_GRAPH], this[INTERNAL_ID]) + this[INTERNAL_GRAPH].detachInstance(n.id) + return this[INTERNAL_API].wrapNode(n.id) + } + // --- Tree --- get parent(): FigmaNodeProxy | null { diff --git a/packages/docs/de/programmable/cli/scripting.md b/packages/docs/de/programmable/cli/scripting.md index 058aae5f9..e3770af57 100644 --- a/packages/docs/de/programmable/cli/scripting.md +++ b/packages/docs/de/programmable/cli/scripting.md @@ -37,6 +37,6 @@ Exakte Namen wie `figma.createFrame()`, `node.appendChild()`, `fontSize` und `la ## Noch nicht kompatibel -Noch nicht angeboten werden unter anderem `node.exportAsync()`, `node.setBoundVariable()`, `node.detachInstance()`, `figma.combineAsVariants()` und die Stil-APIs von Figma. +Noch nicht angeboten werden unter anderem `node.exportAsync()`, `node.setBoundVariable()`, `figma.combineAsVariants()` und die Stil-APIs von Figma. Dafür stehen je nach Aufgabe CLI-Export, Werkzeuge des Kernpakets oder direkte SceneGraph-Hilfsfunktionen zur Verfügung. diff --git a/packages/docs/es/programmable/cli/scripting.md b/packages/docs/es/programmable/cli/scripting.md index ff2757656..9024f2f20 100644 --- a/packages/docs/es/programmable/cli/scripting.md +++ b/packages/docs/es/programmable/cli/scripting.md @@ -110,6 +110,6 @@ Las propiedades habituales se leen y modifican mediante el objeto correspondient ## Limitaciones -Aún no hay equivalentes completos para `node.exportAsync()`, `node.setBoundVariable()`, `node.detachInstance()`, `figma.combineAsVariants()`, estilos de pintura/texto y todas las operaciones booleanas vectoriales. +Aún no hay equivalentes completos para `node.exportAsync()`, `node.setBoundVariable()`, `figma.combineAsVariants()`, estilos de pintura/texto y todas las operaciones booleanas vectoriales. Según la tarea, pueden usarse el comando de exportación, las herramientas del núcleo o las operaciones directas de SceneGraph. diff --git a/packages/docs/fr/programmable/cli/scripting.md b/packages/docs/fr/programmable/cli/scripting.md index 71f80233e..cb446fe05 100644 --- a/packages/docs/fr/programmable/cli/scripting.md +++ b/packages/docs/fr/programmable/cli/scripting.md @@ -55,6 +55,6 @@ Les identifiants exacts comme `figma.currentPage`, `createFrame`, `appendChild`, ## Limites -Il n’existe pas encore d’équivalent complet pour `node.exportAsync()`, `node.setBoundVariable()`, `node.detachInstance()`, `figma.combineAsVariants()`, les styles de peinture/texte et toutes les opérations booléennes vectorielles. +Il n’existe pas encore d’équivalent complet pour `node.exportAsync()`, `node.setBoundVariable()`, `figma.combineAsVariants()`, les styles de peinture/texte et toutes les opérations booléennes vectorielles. Selon le besoin, utilisez aussi la commande d’exportation, les outils du noyau ou les opérations directes de SceneGraph. diff --git a/packages/docs/it/programmable/cli/scripting.md b/packages/docs/it/programmable/cli/scripting.md index f9db829cd..d0e5e5f6e 100644 --- a/packages/docs/it/programmable/cli/scripting.md +++ b/packages/docs/it/programmable/cli/scripting.md @@ -47,4 +47,4 @@ Gli identificatori esatti come `figma.currentPage`, `createFrame`, `appendChild` ## Limiti -Non esistono ancora equivalenti completi per `node.exportAsync()`, `node.setBoundVariable()`, `node.detachInstance()`, `figma.combineAsVariants()`, gli stili e tutte le operazioni booleane vettoriali. +Non esistono ancora equivalenti completi per `node.exportAsync()`, `node.setBoundVariable()`, `figma.combineAsVariants()`, gli stili e tutte le operazioni booleane vettoriali. diff --git a/packages/docs/pl/programmable/cli/scripting.md b/packages/docs/pl/programmable/cli/scripting.md index 6778ae835..eeb4705d5 100644 --- a/packages/docs/pl/programmable/cli/scripting.md +++ b/packages/docs/pl/programmable/cli/scripting.md @@ -106,6 +106,6 @@ Najczęściej używane właściwości można odczytywać i zapisywać przez poś ## Brak pełnej zgodności z Figmą -Nie są jeszcze dostępne między innymi `node.exportAsync()`, `node.setBoundVariable()`, `node.detachInstance()`, `figma.combineAsVariants()` i API stylów Figmy. +Nie są jeszcze dostępne między innymi `node.exportAsync()`, `node.setBoundVariable()`, `figma.combineAsVariants()` i API stylów Figmy. Zamiast nich używaj poleceń eksportu CLI, narzędzi głównego pakietu albo bezpośrednich funkcji pomocniczych SceneGraph. diff --git a/packages/docs/programmable/cli/scripting.md b/packages/docs/programmable/cli/scripting.md index e3c0f0478..564f4e037 100644 --- a/packages/docs/programmable/cli/scripting.md +++ b/packages/docs/programmable/cli/scripting.md @@ -172,7 +172,6 @@ These Figma APIs are not exposed as compatible helpers yet: - `node.exportAsync()` - `node.setBoundVariable(field, variable)` -- `node.detachInstance()` - `figma.combineAsVariants(components, parent)` - Figma style APIs such as `figma.createPaintStyle()` / `figma.createTextStyle()` - Full vector boolean operation parity diff --git a/packages/docs/ru/programmable/cli/scripting.md b/packages/docs/ru/programmable/cli/scripting.md index 2c429608a..30ad54d16 100644 --- a/packages/docs/ru/programmable/cli/scripting.md +++ b/packages/docs/ru/programmable/cli/scripting.md @@ -149,7 +149,6 @@ API намеренно близок к Figma Plugin API, но работает - `node.exportAsync()` - `node.setBoundVariable(field, variable)` -- `node.detachInstance()` - `figma.combineAsVariants(components, parent)` - API стилей Figma, например `figma.createPaintStyle()` и `figma.createTextStyle()` - полная совместимость логических операций над векторами diff --git a/tests/engine/editor/components/library-capabilities.test.ts b/tests/engine/editor/components/library-capabilities.test.ts index 395871b66..e51ab5ad7 100644 --- a/tests/engine/editor/components/library-capabilities.test.ts +++ b/tests/engine/editor/components/library-capabilities.test.ts @@ -53,4 +53,26 @@ describe('library definition capabilities', () => { ReadOnlyLibraryDefinitionError ) }) + + test('does not detach an instance inside a read-only definition', () => { + const graph = new SceneGraph() + const page = graph.getPages()[0] + const icon = graph.createNode('COMPONENT', page.id, { name: 'Icon' }) + const component = graph.createNode('COMPONENT', page.id, { + name: 'Remote button', + librarySource: { + identity: { libraryId: 'design-system', assetKey: 'button', revisionId: 'r1' }, + sourceNodeId: 'source', + readOnly: true + } + }) + const nested = graph.createInstance(icon.id, component.id) + if (!nested) throw new Error('Expected instance') + + const figma = new FigmaAPI(graph) + const nestedProxy = figma.getNodeById(nested.id) + if (!nestedProxy) throw new Error('Expected proxy') + expect(() => nestedProxy.detachInstance()).toThrow(ReadOnlyLibraryDefinitionError) + expect(graph.getNode(nested.id)?.type).toBe('INSTANCE') + }) }) diff --git a/tests/engine/figma/api/components.test.ts b/tests/engine/figma/api/components.test.ts index 4b22235ac..4a9f8970f 100644 --- a/tests/engine/figma/api/components.test.ts +++ b/tests/engine/figma/api/components.test.ts @@ -62,6 +62,31 @@ describe('components', () => { expect(expectDefined(instance.mainComponent, 'instance main component').id).toBe(comp.id) }) + test('detachInstance turns an instance into a frame with its content', () => { + const api = createAPI() + const component = api.createComponent() + component.resize(80, 40) + component.appendChild(Object.assign(api.createText(), { name: 'Label', characters: 'Default' })) + const instance = component.createInstance() + instance.x = 200 + + const detached = instance.detachInstance() + + expect(detached.id).toBe(instance.id) + expect(detached.type).toBe('FRAME') + expect(detached.mainComponent).toBeNull() + expect([detached.x, detached.width, detached.height]).toEqual([200, 80, 40]) + expect(detached.children.map((child) => child.name)).toEqual(['Label']) + expect(api.getNodeById(component.id)?.type).toBe('COMPONENT') + }) + + test('detachInstance rejects nodes that are not instances', () => { + const api = createAPI() + expect(() => api.createFrame().detachInstance()).toThrow( + 'detachInstance() can only be called on instances' + ) + }) + test('async lookups resolve like their synchronous forms', async () => { const api = createAPI() const component = api.createComponent()