@nnao45/figma-use 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +991 -0
- package/LICENSE +22 -0
- package/README.md +569 -0
- package/SKILL.md +683 -0
- package/bin/figma-use.js +9 -0
- package/dist/cli/index.js +496 -0
- package/package.json +87 -0
- package/packages/cli/src/render/component-set.tsx +157 -0
- package/packages/cli/src/render/components.tsx +115 -0
- package/packages/cli/src/render/icon.ts +166 -0
- package/packages/cli/src/render/index.ts +47 -0
- package/packages/cli/src/render/jsx-dev-runtime.ts +6 -0
- package/packages/cli/src/render/jsx-runtime.ts +90 -0
- package/packages/cli/src/render/mini-react.ts +33 -0
- package/packages/cli/src/render/render-from-string.ts +121 -0
- package/packages/cli/src/render/render-jsx.ts +44 -0
- package/packages/cli/src/render/tree.ts +148 -0
- package/packages/cli/src/render/vars.ts +186 -0
- package/packages/cli/src/render/widget-renderer.ts +163 -0
- package/packages/plugin/src/main.ts +2747 -0
- package/packages/plugin/src/query.ts +253 -0
- package/packages/plugin/src/rpc.ts +5238 -0
- package/packages/plugin/src/ui.html +25 -0
- package/packages/plugin/src/ui.ts +74 -0
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nnao45/figma-use",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Control Figma from the command line. Full read/write access for AI agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-agents",
|
|
8
|
+
"automation",
|
|
9
|
+
"cli",
|
|
10
|
+
"design",
|
|
11
|
+
"figma",
|
|
12
|
+
"plugin"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/nnao45/figma-use#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/nnao45/figma-use/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "nnao45",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/nnao45/figma-use.git"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"figma-fast": "bin/figma-fast.js",
|
|
29
|
+
"figma-use": "bin/figma-use.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"README.md",
|
|
35
|
+
"SKILL.md",
|
|
36
|
+
"dist/cli",
|
|
37
|
+
"packages/plugin/src",
|
|
38
|
+
"packages/cli/src/color.ts",
|
|
39
|
+
"packages/cli/src/render"
|
|
40
|
+
],
|
|
41
|
+
"type": "module",
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"exports": {
|
|
44
|
+
".": "./dist/cli/index.js",
|
|
45
|
+
"./render": "./packages/cli/src/render/index.ts",
|
|
46
|
+
"./components": "./packages/cli/src/render/components.tsx"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "bun run build:cli",
|
|
50
|
+
"build:cli": "bun build packages/cli/src/index.ts --outdir dist/cli --target node --minify --external esbuild --external typescript --external @modelcontextprotocol/sdk --external oxfmt --external whaticon --external pngjs --external pixelmatch",
|
|
51
|
+
"test": "cd packages/cli && bun test",
|
|
52
|
+
"lint": "oxlint packages/",
|
|
53
|
+
"format": "oxfmt --write packages/",
|
|
54
|
+
"prepublishOnly": "bun run build",
|
|
55
|
+
"build:fast": "bun build packages/cli/src/fast.ts --outfile dist/fast.mjs --target node --minify"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"agentfmt": "^0.1.3",
|
|
59
|
+
"citty": "^0.1.6",
|
|
60
|
+
"consola": "^3.4.2",
|
|
61
|
+
"diff": "^8.0.3",
|
|
62
|
+
"esbuild": "^0.25.4",
|
|
63
|
+
"fontoxpath": "^3.34.0",
|
|
64
|
+
"postcss": "^8.5.6",
|
|
65
|
+
"svgpath": "^2.6.0",
|
|
66
|
+
"typescript": "^5.8.3",
|
|
67
|
+
"whaticon": "0.1.2",
|
|
68
|
+
"zod": "^4.3.6"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@figma/plugin-typings": "^1.122.0",
|
|
72
|
+
"@iconify/core": "^4.1.0",
|
|
73
|
+
"@iconify/types": "^2.0.0",
|
|
74
|
+
"@iconify/utils": "^3.1.0",
|
|
75
|
+
"@types/bun": "^1.3.6",
|
|
76
|
+
"@types/pngjs": "^6.0.5",
|
|
77
|
+
"@types/react": "^19.2.9",
|
|
78
|
+
"@types/ws": "^8.18.1",
|
|
79
|
+
"ajv": "^8.17.1",
|
|
80
|
+
"ajv-formats": "^3.0.1",
|
|
81
|
+
"oxfmt": "^0.26.0",
|
|
82
|
+
"oxlint": "^1.39.0",
|
|
83
|
+
"pixelmatch": "^7.1.0",
|
|
84
|
+
"pngjs": "^7.0.0",
|
|
85
|
+
"react": "19"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentSet (variants) support
|
|
3
|
+
*
|
|
4
|
+
* ## How ComponentSet works in Figma's multiplayer protocol
|
|
5
|
+
*
|
|
6
|
+
* ComponentSet is NOT a separate node type - it's a FRAME with special fields:
|
|
7
|
+
* - type = FRAME (4)
|
|
8
|
+
* - isStateGroup = true (field 225)
|
|
9
|
+
* - stateGroupPropertyValueOrders (field 238) = [{property: "variant", values: ["A", "B"]}]
|
|
10
|
+
*
|
|
11
|
+
* Children are SYMBOL (Component) nodes with names like "variant=Primary, size=Large".
|
|
12
|
+
* Figma auto-generates componentPropertyDefinitions from these names.
|
|
13
|
+
*
|
|
14
|
+
* ## Why Instances are created via Plugin API
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT: Instance nodes with symbolData.symbolID CANNOT be created via multiplayer
|
|
17
|
+
* when linking to components in the same batch. Figma reassigns GUIDs on receive,
|
|
18
|
+
* breaking the symbolID references.
|
|
19
|
+
*
|
|
20
|
+
* Example: We send SYMBOL with localID=100, Instance with symbolData.symbolID=100.
|
|
21
|
+
* Figma receives and assigns new IDs: SYMBOL becomes 200, but Instance still
|
|
22
|
+
* references 100 → broken link.
|
|
23
|
+
*
|
|
24
|
+
* This works for defineComponent() because Component and first Instance are adjacent
|
|
25
|
+
* in the node tree, but fails for ComponentSet where variant components are created
|
|
26
|
+
* first, then instances are created as siblings of the ComponentSet.
|
|
27
|
+
*
|
|
28
|
+
* Solution: Create the ComponentSet and variant Components via multiplayer (fast),
|
|
29
|
+
* then create Instances via Plugin API in trigger-layout (correct linking).
|
|
30
|
+
* The pendingComponentSetInstances array passes instance specs to the plugin.
|
|
31
|
+
*
|
|
32
|
+
* Discovered through protocol sniffing - see scripts/sniff-ws.ts
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import * as React from './mini-react.ts'
|
|
36
|
+
|
|
37
|
+
import type { FC, ReactElement } from './mini-react.ts'
|
|
38
|
+
|
|
39
|
+
type VariantDef = Record<string, readonly string[]>
|
|
40
|
+
type VariantProps<V extends VariantDef> = { [K in keyof V]: V[K][number] }
|
|
41
|
+
|
|
42
|
+
interface ComponentSetDef<V extends VariantDef> {
|
|
43
|
+
name: string
|
|
44
|
+
variants: V
|
|
45
|
+
render: (props: VariantProps<V>) => ReactElement
|
|
46
|
+
symbol: symbol
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Use global registry to avoid module duplication issues between bundled CLI and source imports
|
|
50
|
+
const REGISTRY_KEY = '__figma_use_component_set_registry__'
|
|
51
|
+
const componentSetRegistry: Map<symbol, ComponentSetDef<VariantDef>> = ((
|
|
52
|
+
globalThis as Record<string, unknown>
|
|
53
|
+
)[REGISTRY_KEY] as Map<symbol, ComponentSetDef<VariantDef>>) ||
|
|
54
|
+
((globalThis as Record<string, unknown>)[REGISTRY_KEY] = new Map<
|
|
55
|
+
symbol,
|
|
56
|
+
ComponentSetDef<VariantDef>
|
|
57
|
+
>())
|
|
58
|
+
|
|
59
|
+
export function resetComponentSetRegistry() {
|
|
60
|
+
componentSetRegistry.clear()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getComponentSetRegistry() {
|
|
64
|
+
return componentSetRegistry
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Define a component with variants (ComponentSet)
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const Button = defineComponentSet('Button', {
|
|
72
|
+
* variant: ['Primary', 'Secondary'] as const,
|
|
73
|
+
* size: ['Small', 'Large'] as const,
|
|
74
|
+
* }, ({ variant, size }) => (
|
|
75
|
+
* <Frame style={{ padding: size === 'Large' ? 16 : 8 }}>
|
|
76
|
+
* <Text>{variant}</Text>
|
|
77
|
+
* </Frame>
|
|
78
|
+
* ))
|
|
79
|
+
*
|
|
80
|
+
* <Button variant="Primary" size="Large" />
|
|
81
|
+
*/
|
|
82
|
+
export function defineComponentSet<V extends VariantDef>(
|
|
83
|
+
name: string,
|
|
84
|
+
variants: V,
|
|
85
|
+
render: (props: VariantProps<V>) => ReactElement
|
|
86
|
+
): FC<Partial<VariantProps<V>> & { style?: Record<string, unknown> }> {
|
|
87
|
+
const sym = Symbol(name)
|
|
88
|
+
componentSetRegistry.set(sym, {
|
|
89
|
+
name,
|
|
90
|
+
variants,
|
|
91
|
+
render,
|
|
92
|
+
symbol: sym
|
|
93
|
+
} as ComponentSetDef<VariantDef>)
|
|
94
|
+
|
|
95
|
+
const VariantInstance: FC<Partial<VariantProps<V>> & { style?: Record<string, unknown> }> = (
|
|
96
|
+
props
|
|
97
|
+
) => {
|
|
98
|
+
const { style, ...variantProps } = props
|
|
99
|
+
return React.createElement('__component_set_instance__', {
|
|
100
|
+
__componentSetSymbol: sym,
|
|
101
|
+
__componentSetName: name,
|
|
102
|
+
__variantProps: variantProps,
|
|
103
|
+
style
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
;(VariantInstance as unknown as { displayName: string }).displayName = name
|
|
107
|
+
|
|
108
|
+
return VariantInstance
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate all variant combinations
|
|
113
|
+
*/
|
|
114
|
+
export function generateVariantCombinations<V extends VariantDef>(
|
|
115
|
+
variants: V
|
|
116
|
+
): Array<VariantProps<V>> {
|
|
117
|
+
const keys = Object.keys(variants) as (keyof V)[]
|
|
118
|
+
if (keys.length === 0) return [{}] as Array<VariantProps<V>>
|
|
119
|
+
|
|
120
|
+
const result: Array<VariantProps<V>> = []
|
|
121
|
+
|
|
122
|
+
function combine(index: number, current: Partial<VariantProps<V>>) {
|
|
123
|
+
if (index === keys.length) {
|
|
124
|
+
result.push(current as VariantProps<V>)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const key = keys[index]!
|
|
129
|
+
for (const value of variants[key]!) {
|
|
130
|
+
combine(index + 1, { ...current, [key]: value })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
combine(0, {})
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build variant name string for Figma component (e.g., "variant=Primary, size=Large")
|
|
140
|
+
*/
|
|
141
|
+
export function buildVariantName(props: Record<string, string>): string {
|
|
142
|
+
return Object.entries(props)
|
|
143
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
144
|
+
.join(', ')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build stateGroupPropertyValueOrders for ComponentSet
|
|
149
|
+
*/
|
|
150
|
+
export function buildStateGroupPropertyValueOrders(
|
|
151
|
+
variants: VariantDef
|
|
152
|
+
): Array<{ property: string; values: string[] }> {
|
|
153
|
+
return Object.entries(variants).map(([property, values]) => ({
|
|
154
|
+
property,
|
|
155
|
+
values: [...values]
|
|
156
|
+
}))
|
|
157
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma JSX Components
|
|
3
|
+
*
|
|
4
|
+
* Returns TreeNode objects for Figma Widget API rendering.
|
|
5
|
+
* Use @ts-expect-error in .figma.tsx files if TypeScript complains about JSX types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { node, type BaseProps, type TextProps, type TreeNode } from './tree.ts'
|
|
9
|
+
|
|
10
|
+
// Core components
|
|
11
|
+
export function Frame(props: BaseProps): TreeNode {
|
|
12
|
+
return node('frame', props)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Text(props: TextProps): TreeNode {
|
|
16
|
+
return node('text', props)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Rectangle(props: BaseProps): TreeNode {
|
|
20
|
+
return node('rectangle', props)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Ellipse(props: BaseProps): TreeNode {
|
|
24
|
+
return node('ellipse', props)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Line(props: BaseProps): TreeNode {
|
|
28
|
+
return node('line', props)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Image(props: BaseProps & { src: string }): TreeNode {
|
|
32
|
+
return node('image', props)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SVG(props: BaseProps & { src: string }): TreeNode {
|
|
36
|
+
return node('svg', props)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Star(props: BaseProps & { points?: number; innerRadius?: number }): TreeNode {
|
|
40
|
+
return node('star', props)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Polygon(props: BaseProps & { pointCount?: number }): TreeNode {
|
|
44
|
+
return node('polygon', props)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Vector(props: BaseProps): TreeNode {
|
|
48
|
+
return node('vector', props)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Group(props: BaseProps): TreeNode {
|
|
52
|
+
return node('group', props)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function Section(props: BaseProps): TreeNode {
|
|
56
|
+
return node('section', props)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Aliases
|
|
60
|
+
export const View = Frame
|
|
61
|
+
export const Rect = Rectangle
|
|
62
|
+
export const Component = Frame
|
|
63
|
+
export const Instance = Frame
|
|
64
|
+
export const Page = Frame
|
|
65
|
+
|
|
66
|
+
// Intrinsic elements
|
|
67
|
+
export const INTRINSIC_ELEMENTS = [
|
|
68
|
+
'frame',
|
|
69
|
+
'text',
|
|
70
|
+
'rectangle',
|
|
71
|
+
'ellipse',
|
|
72
|
+
'line',
|
|
73
|
+
'image',
|
|
74
|
+
'svg',
|
|
75
|
+
'star',
|
|
76
|
+
'polygon',
|
|
77
|
+
'vector',
|
|
78
|
+
'group',
|
|
79
|
+
'section'
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
// Variables
|
|
83
|
+
export {
|
|
84
|
+
defineVars,
|
|
85
|
+
figmaVar,
|
|
86
|
+
isVariable,
|
|
87
|
+
loadVariablesIntoRegistry,
|
|
88
|
+
isRegistryLoaded,
|
|
89
|
+
type FigmaVariable
|
|
90
|
+
} from './vars.ts'
|
|
91
|
+
|
|
92
|
+
// Legacy component registry
|
|
93
|
+
const componentRegistry = new Map<symbol, { name: string; element: unknown }>()
|
|
94
|
+
export const resetComponentRegistry = () => componentRegistry.clear()
|
|
95
|
+
export const getComponentRegistry = () => componentRegistry
|
|
96
|
+
|
|
97
|
+
export function defineComponent(name: string, element: unknown) {
|
|
98
|
+
const sym = Symbol(name)
|
|
99
|
+
componentRegistry.set(sym, { name, element })
|
|
100
|
+
return () => element
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Icon
|
|
104
|
+
export interface IconProps {
|
|
105
|
+
icon: string
|
|
106
|
+
size?: number
|
|
107
|
+
color?: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function Icon(props: IconProps) {
|
|
111
|
+
return { __icon: true, ...props }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Re-export types
|
|
115
|
+
export type { BaseProps, TextProps, StyleProps, TreeNode } from './tree.ts'
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { loadIcon } from '@iconify/core/lib/api/icons'
|
|
2
|
+
import { setAPIModule } from '@iconify/core/lib/api/modules'
|
|
3
|
+
import { fetchAPIModule } from '@iconify/core/lib/api/modules/fetch'
|
|
4
|
+
import { iconToSVG } from '@iconify/utils'
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from './mini-react.ts'
|
|
7
|
+
import type { Props, ReactElement } from './tree.ts'
|
|
8
|
+
import type { IconifyIcon } from '@iconify/types'
|
|
9
|
+
|
|
10
|
+
// Initialize API module
|
|
11
|
+
setAPIModule('', fetchAPIModule)
|
|
12
|
+
|
|
13
|
+
export interface IconData {
|
|
14
|
+
svg: string
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
body: string
|
|
18
|
+
viewBox: { left: number; top: number; width: number; height: number }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const iconCache = new Map<string, IconData>()
|
|
22
|
+
|
|
23
|
+
// Raw icon data cache (before size transformation)
|
|
24
|
+
const rawIconCache = new Map<string, IconifyIcon>()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load raw icon data (without size transformation)
|
|
28
|
+
*/
|
|
29
|
+
async function loadRawIcon(name: string): Promise<IconifyIcon | null> {
|
|
30
|
+
if (rawIconCache.has(name)) {
|
|
31
|
+
return rawIconCache.get(name)!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const icon = await loadIcon(name)
|
|
35
|
+
if (!icon) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
rawIconCache.set(name, icon)
|
|
40
|
+
return icon
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load icon from Iconify and return SVG string
|
|
45
|
+
* @param name Icon name in format "prefix:name" (e.g., "mdi:home", "lucide:star")
|
|
46
|
+
* @param size Optional size (default: 24)
|
|
47
|
+
*/
|
|
48
|
+
export async function loadIconSvg(name: string, size: number = 24): Promise<IconData | null> {
|
|
49
|
+
const cacheKey = `${name}@${size}`
|
|
50
|
+
|
|
51
|
+
if (iconCache.has(cacheKey)) {
|
|
52
|
+
return iconCache.get(cacheKey)!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const icon = await loadRawIcon(name)
|
|
56
|
+
if (!icon) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = iconToSVG(icon, { height: size, width: size })
|
|
61
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" ${Object.entries(result.attributes)
|
|
62
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
63
|
+
.join(' ')}>${result.body}</svg>`
|
|
64
|
+
|
|
65
|
+
const data: IconData = {
|
|
66
|
+
svg,
|
|
67
|
+
width: size,
|
|
68
|
+
height: size,
|
|
69
|
+
body: result.body,
|
|
70
|
+
viewBox: {
|
|
71
|
+
left: result.viewBox[0],
|
|
72
|
+
top: result.viewBox[1],
|
|
73
|
+
width: result.viewBox[2],
|
|
74
|
+
height: result.viewBox[3]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
iconCache.set(cacheKey, data)
|
|
79
|
+
return data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get cached icon data (synchronous, for use in React components)
|
|
84
|
+
* Returns null if icon not preloaded
|
|
85
|
+
*/
|
|
86
|
+
export function getIconData(name: string, size: number = 24): IconData | null {
|
|
87
|
+
return iconCache.get(`${name}@${size}`) || null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Preload icons for use in JSX render
|
|
92
|
+
* Call before rendering to ensure icons are available synchronously
|
|
93
|
+
*/
|
|
94
|
+
export async function preloadIcons(icons: Array<{ name: string; size?: number }>): Promise<void> {
|
|
95
|
+
await Promise.all(icons.map(({ name, size }) => loadIconSvg(name, size || 24)))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get list of popular icon sets
|
|
100
|
+
*/
|
|
101
|
+
export const iconSets = {
|
|
102
|
+
mdi: 'Material Design Icons',
|
|
103
|
+
lucide: 'Lucide',
|
|
104
|
+
heroicons: 'Heroicons',
|
|
105
|
+
'heroicons-outline': 'Heroicons Outline',
|
|
106
|
+
'heroicons-solid': 'Heroicons Solid',
|
|
107
|
+
tabler: 'Tabler Icons',
|
|
108
|
+
'fa-solid': 'Font Awesome Solid',
|
|
109
|
+
'fa-regular': 'Font Awesome Regular',
|
|
110
|
+
'fa-brands': 'Font Awesome Brands',
|
|
111
|
+
ri: 'Remix Icon',
|
|
112
|
+
ph: 'Phosphor',
|
|
113
|
+
'ph-bold': 'Phosphor Bold',
|
|
114
|
+
'ph-fill': 'Phosphor Fill',
|
|
115
|
+
carbon: 'Carbon',
|
|
116
|
+
fluent: 'Fluent UI',
|
|
117
|
+
ion: 'Ionicons',
|
|
118
|
+
bi: 'Bootstrap Icons'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recursively collect Icon primitives from React element tree
|
|
123
|
+
*/
|
|
124
|
+
export function collectIcons(element: ReactNode): Array<{ name: string; size?: number }> {
|
|
125
|
+
const icons: Array<{ name: string; size?: number }> = []
|
|
126
|
+
|
|
127
|
+
function traverse(node: ReactNode): void {
|
|
128
|
+
if (!node || typeof node !== 'object') return
|
|
129
|
+
|
|
130
|
+
if (Array.isArray(node)) {
|
|
131
|
+
node.forEach(traverse)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const el = node as ReactElement
|
|
136
|
+
if (!el.type) return
|
|
137
|
+
|
|
138
|
+
if (el.type === 'icon') {
|
|
139
|
+
const props = el.props as { icon?: string; size?: number }
|
|
140
|
+
if (props.icon) {
|
|
141
|
+
icons.push({ name: props.icon, size: props.size })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof el.type === 'function') {
|
|
146
|
+
try {
|
|
147
|
+
const rendered = (el.type as (p: Props) => ReactNode)(el.props as Props)
|
|
148
|
+
if (rendered) traverse(rendered)
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore render errors during collection
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const props = el.props as { children?: ReactNode }
|
|
155
|
+
if (props.children) {
|
|
156
|
+
if (Array.isArray(props.children)) {
|
|
157
|
+
props.children.forEach(traverse)
|
|
158
|
+
} else {
|
|
159
|
+
traverse(props.children)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
traverse(element)
|
|
165
|
+
return icons
|
|
166
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React → Figma Renderer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { renderWithWidgetApi } from './widget-renderer.ts'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
Frame,
|
|
9
|
+
Text,
|
|
10
|
+
Rectangle,
|
|
11
|
+
Ellipse,
|
|
12
|
+
Line,
|
|
13
|
+
Image,
|
|
14
|
+
SVG,
|
|
15
|
+
View,
|
|
16
|
+
Rect,
|
|
17
|
+
Star,
|
|
18
|
+
Polygon,
|
|
19
|
+
Vector,
|
|
20
|
+
Component,
|
|
21
|
+
Instance,
|
|
22
|
+
Group,
|
|
23
|
+
Page,
|
|
24
|
+
Icon,
|
|
25
|
+
INTRINSIC_ELEMENTS,
|
|
26
|
+
defineVars,
|
|
27
|
+
figmaVar,
|
|
28
|
+
isVariable,
|
|
29
|
+
loadVariablesIntoRegistry,
|
|
30
|
+
isRegistryLoaded,
|
|
31
|
+
type FigmaVariable,
|
|
32
|
+
defineComponent,
|
|
33
|
+
resetComponentRegistry,
|
|
34
|
+
getComponentRegistry
|
|
35
|
+
} from './components.tsx'
|
|
36
|
+
|
|
37
|
+
export { preloadIcons, loadIconSvg, getIconData, collectIcons } from './icon.ts'
|
|
38
|
+
|
|
39
|
+
export { renderJsx } from './render-jsx.ts'
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
defineComponentSet,
|
|
43
|
+
resetComponentSetRegistry,
|
|
44
|
+
getComponentSetRegistry
|
|
45
|
+
} from './component-set.tsx'
|
|
46
|
+
|
|
47
|
+
export { buildComponent, readStdin, renderFromString } from './render-from-string.ts'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom JSX Runtime for Figma components
|
|
3
|
+
*
|
|
4
|
+
* This module provides jsx/jsxs functions that produce TreeNode objects
|
|
5
|
+
* instead of React elements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { node, type BaseProps, type TreeNode, type TextProps } from './tree.ts'
|
|
9
|
+
|
|
10
|
+
export function jsx(type: string | ((props: BaseProps) => TreeNode), props: BaseProps): TreeNode {
|
|
11
|
+
if (typeof type === 'function') {
|
|
12
|
+
return type(props)
|
|
13
|
+
}
|
|
14
|
+
return node(type, props)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const jsxs = jsx
|
|
18
|
+
export const jsxDEV = jsx
|
|
19
|
+
|
|
20
|
+
// Fragment just returns children
|
|
21
|
+
export function Fragment({ children }: { children?: unknown }): TreeNode {
|
|
22
|
+
return node('fragment', { children } as BaseProps)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// SVG element props
|
|
26
|
+
interface SvgProps {
|
|
27
|
+
width?: string | number
|
|
28
|
+
height?: string | number
|
|
29
|
+
viewBox?: string
|
|
30
|
+
fill?: string
|
|
31
|
+
children?: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PathProps {
|
|
35
|
+
d: string
|
|
36
|
+
fill?: string
|
|
37
|
+
stroke?: string
|
|
38
|
+
strokeWidth?: string | number
|
|
39
|
+
fillRule?: string
|
|
40
|
+
clipRule?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RectProps {
|
|
44
|
+
x?: string | number
|
|
45
|
+
y?: string | number
|
|
46
|
+
width?: string | number
|
|
47
|
+
height?: string | number
|
|
48
|
+
rx?: string | number
|
|
49
|
+
ry?: string | number
|
|
50
|
+
fill?: string
|
|
51
|
+
stroke?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CircleProps {
|
|
55
|
+
cx?: string | number
|
|
56
|
+
cy?: string | number
|
|
57
|
+
r?: string | number
|
|
58
|
+
fill?: string
|
|
59
|
+
stroke?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// JSX namespace for TypeScript
|
|
63
|
+
export namespace JSX {
|
|
64
|
+
export type Element = TreeNode
|
|
65
|
+
|
|
66
|
+
export interface IntrinsicElements {
|
|
67
|
+
frame: BaseProps
|
|
68
|
+
text: TextProps
|
|
69
|
+
rectangle: BaseProps
|
|
70
|
+
ellipse: BaseProps
|
|
71
|
+
line: BaseProps
|
|
72
|
+
star: BaseProps & { points?: number; innerRadius?: number }
|
|
73
|
+
polygon: BaseProps & { pointCount?: number }
|
|
74
|
+
vector: BaseProps
|
|
75
|
+
group: BaseProps
|
|
76
|
+
image: BaseProps & { src: string }
|
|
77
|
+
// Figma SVG component with src string
|
|
78
|
+
SVG: BaseProps & { src: string }
|
|
79
|
+
// Native SVG elements (inline)
|
|
80
|
+
svg: SvgProps
|
|
81
|
+
path: PathProps
|
|
82
|
+
rect: RectProps
|
|
83
|
+
circle: CircleProps
|
|
84
|
+
g: { children?: unknown; fill?: string; stroke?: string }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ElementChildrenAttribute {
|
|
88
|
+
children: {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Minimal React-compatible createElement for JSX rendering
|
|
2
|
+
// Replaces 86KB react package with ~200 bytes
|
|
3
|
+
|
|
4
|
+
export type ReactElement = {
|
|
5
|
+
type: string | Function
|
|
6
|
+
props: Record<string, unknown> & { children?: ReactNode[] }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ReactNode = ReactElement | string | number | null | undefined | ReactNode[]
|
|
10
|
+
|
|
11
|
+
export type FC<P = Record<string, unknown>> = (props: P) => ReactElement
|
|
12
|
+
|
|
13
|
+
export function createElement(
|
|
14
|
+
type: string | Function,
|
|
15
|
+
props: Record<string, unknown> | null,
|
|
16
|
+
...children: ReactNode[]
|
|
17
|
+
): ReactElement {
|
|
18
|
+
const flatChildren = children.flat()
|
|
19
|
+
return {
|
|
20
|
+
type,
|
|
21
|
+
props: {
|
|
22
|
+
...props,
|
|
23
|
+
children:
|
|
24
|
+
flatChildren.length === 1
|
|
25
|
+
? (flatChildren as ReactNode[])
|
|
26
|
+
: flatChildren.length > 0
|
|
27
|
+
? flatChildren
|
|
28
|
+
: undefined
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default { createElement }
|