feat(figma-api): add detachInstance() to instances (#778)
instance.detachInstance() turns an instance into a frame that keeps its content, as in Figma, from scripts run through eval. It reuses the graph's shared detach implementation, asserts editability like the proxy's other mutations, and joins the instance surface check against @figma/plugin-typings.
This commit is contained in:
parent
bb86d2ad7f
commit
2c9bb8fcd7
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ type InstancePropertySurfaceMatch = Expect<
|
|||
| 'isExposedInstance'
|
||||
| 'exposedInstances'
|
||||
| 'getMainComponentAsync'
|
||||
| 'detachInstance'
|
||||
>,
|
||||
Pick<
|
||||
InstanceNode,
|
||||
|
|
@ -80,6 +81,7 @@ type InstancePropertySurfaceMatch = Expect<
|
|||
| 'isExposedInstance'
|
||||
| 'exposedInstances'
|
||||
| 'getMainComponentAsync'
|
||||
| 'detachInstance'
|
||||
>
|
||||
>
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()`
|
||||
- полная совместимость логических операций над векторами
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue