@dannote/figma-use 0.5.0 → 0.5.2
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 +19 -0
- package/SKILL.md +88 -209
- package/dist/cli/index.js +38 -5
- package/dist/proxy/index.js +21 -32
- package/package.json +7 -1
- package/packages/cli/src/render/component-set.tsx +138 -0
- package/packages/cli/src/render/components.tsx +155 -0
- package/packages/cli/src/render/index.ts +47 -0
- package/packages/cli/src/render/reconciler.ts +845 -0
- package/packages/cli/src/render/vars.ts +185 -0
|
@@ -0,0 +1,138 @@
|
|
|
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 'react'
|
|
36
|
+
|
|
37
|
+
type VariantDef = Record<string, readonly string[]>
|
|
38
|
+
type VariantProps<V extends VariantDef> = { [K in keyof V]: V[K][number] }
|
|
39
|
+
|
|
40
|
+
interface ComponentSetDef<V extends VariantDef> {
|
|
41
|
+
name: string
|
|
42
|
+
variants: V
|
|
43
|
+
render: (props: VariantProps<V>) => React.ReactElement
|
|
44
|
+
symbol: symbol
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const componentSetRegistry = new Map<symbol, ComponentSetDef<VariantDef>>()
|
|
48
|
+
|
|
49
|
+
export function resetComponentSetRegistry() {
|
|
50
|
+
componentSetRegistry.clear()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getComponentSetRegistry() {
|
|
54
|
+
return componentSetRegistry
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Define a component with variants (ComponentSet)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const Button = defineComponentSet('Button', {
|
|
62
|
+
* variant: ['Primary', 'Secondary'] as const,
|
|
63
|
+
* size: ['Small', 'Large'] as const,
|
|
64
|
+
* }, ({ variant, size }) => (
|
|
65
|
+
* <Frame style={{ padding: size === 'Large' ? 16 : 8 }}>
|
|
66
|
+
* <Text>{variant}</Text>
|
|
67
|
+
* </Frame>
|
|
68
|
+
* ))
|
|
69
|
+
*
|
|
70
|
+
* <Button variant="Primary" size="Large" />
|
|
71
|
+
*/
|
|
72
|
+
export function defineComponentSet<V extends VariantDef>(
|
|
73
|
+
name: string,
|
|
74
|
+
variants: V,
|
|
75
|
+
render: (props: VariantProps<V>) => React.ReactElement
|
|
76
|
+
): React.FC<Partial<VariantProps<V>> & { style?: Record<string, unknown> }> {
|
|
77
|
+
const sym = Symbol(name)
|
|
78
|
+
componentSetRegistry.set(sym, { name, variants, render, symbol: sym } as ComponentSetDef<VariantDef>)
|
|
79
|
+
|
|
80
|
+
const VariantInstance: React.FC<Partial<VariantProps<V>> & { style?: Record<string, unknown> }> = (props) => {
|
|
81
|
+
const { style, ...variantProps } = props
|
|
82
|
+
return React.createElement('__component_set_instance__', {
|
|
83
|
+
__componentSetSymbol: sym,
|
|
84
|
+
__componentSetName: name,
|
|
85
|
+
__variantProps: variantProps,
|
|
86
|
+
style,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
VariantInstance.displayName = name
|
|
90
|
+
|
|
91
|
+
return VariantInstance
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate all variant combinations
|
|
96
|
+
*/
|
|
97
|
+
export function generateVariantCombinations<V extends VariantDef>(
|
|
98
|
+
variants: V
|
|
99
|
+
): Array<VariantProps<V>> {
|
|
100
|
+
const keys = Object.keys(variants) as (keyof V)[]
|
|
101
|
+
if (keys.length === 0) return [{}] as Array<VariantProps<V>>
|
|
102
|
+
|
|
103
|
+
const result: Array<VariantProps<V>> = []
|
|
104
|
+
|
|
105
|
+
function combine(index: number, current: Partial<VariantProps<V>>) {
|
|
106
|
+
if (index === keys.length) {
|
|
107
|
+
result.push(current as VariantProps<V>)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const key = keys[index]
|
|
112
|
+
for (const value of variants[key]) {
|
|
113
|
+
combine(index + 1, { ...current, [key]: value })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
combine(0, {})
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build variant name string for Figma component (e.g., "variant=Primary, size=Large")
|
|
123
|
+
*/
|
|
124
|
+
export function buildVariantName(props: Record<string, string>): string {
|
|
125
|
+
return Object.entries(props)
|
|
126
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
127
|
+
.join(', ')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build stateGroupPropertyValueOrders for ComponentSet
|
|
132
|
+
*/
|
|
133
|
+
export function buildStateGroupPropertyValueOrders(variants: VariantDef): Array<{property: string, values: string[]}> {
|
|
134
|
+
return Object.entries(variants).map(([property, values]) => ({
|
|
135
|
+
property,
|
|
136
|
+
values: [...values]
|
|
137
|
+
}))
|
|
138
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma-like React components
|
|
3
|
+
*
|
|
4
|
+
* API inspired by react-figma but outputs JSON instead of Figma nodes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as React from 'react'
|
|
8
|
+
|
|
9
|
+
// Re-export defineVars for use in .figma.tsx files
|
|
10
|
+
export {
|
|
11
|
+
defineVars,
|
|
12
|
+
figmaVar,
|
|
13
|
+
isVariable,
|
|
14
|
+
loadVariablesIntoRegistry,
|
|
15
|
+
isRegistryLoaded,
|
|
16
|
+
type FigmaVariable
|
|
17
|
+
} from './vars.ts'
|
|
18
|
+
|
|
19
|
+
// Component registry - tracks defined components and their instances
|
|
20
|
+
interface ComponentDef {
|
|
21
|
+
name: string
|
|
22
|
+
element: React.ReactElement
|
|
23
|
+
guid?: string // Set after first render creates the component
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const componentRegistry = new Map<symbol, ComponentDef>()
|
|
27
|
+
|
|
28
|
+
export function resetComponentRegistry() {
|
|
29
|
+
componentRegistry.clear()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getComponentRegistry() {
|
|
33
|
+
return componentRegistry
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Define a reusable Figma component
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const Button = defineComponent('Button',
|
|
41
|
+
* <Frame style={{ padding: 12, backgroundColor: "#3B82F6" }}>
|
|
42
|
+
* <Text style={{ color: "#FFF" }}>Click</Text>
|
|
43
|
+
* </Frame>
|
|
44
|
+
* )
|
|
45
|
+
*
|
|
46
|
+
* export default () => (
|
|
47
|
+
* <Frame>
|
|
48
|
+
* <Button />
|
|
49
|
+
* <Button />
|
|
50
|
+
* </Frame>
|
|
51
|
+
* )
|
|
52
|
+
*/
|
|
53
|
+
export function defineComponent<P extends BaseProps = BaseProps>(
|
|
54
|
+
name: string,
|
|
55
|
+
element: React.ReactElement
|
|
56
|
+
): React.FC<P> {
|
|
57
|
+
const sym = Symbol(name)
|
|
58
|
+
componentRegistry.set(sym, { name, element })
|
|
59
|
+
|
|
60
|
+
// Return a component that renders as a special marker
|
|
61
|
+
const ComponentInstance: React.FC<P> = (props) => {
|
|
62
|
+
return React.createElement('__component_instance__', {
|
|
63
|
+
__componentSymbol: sym,
|
|
64
|
+
__componentName: name,
|
|
65
|
+
...props,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
ComponentInstance.displayName = name
|
|
69
|
+
|
|
70
|
+
return ComponentInstance
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Style types
|
|
74
|
+
interface Style {
|
|
75
|
+
width?: number | string
|
|
76
|
+
height?: number | string
|
|
77
|
+
x?: number
|
|
78
|
+
y?: number
|
|
79
|
+
flexDirection?: 'row' | 'column'
|
|
80
|
+
justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between'
|
|
81
|
+
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
|
|
82
|
+
gap?: number
|
|
83
|
+
padding?: number
|
|
84
|
+
paddingTop?: number
|
|
85
|
+
paddingRight?: number
|
|
86
|
+
paddingBottom?: number
|
|
87
|
+
paddingLeft?: number
|
|
88
|
+
backgroundColor?: string
|
|
89
|
+
opacity?: number
|
|
90
|
+
borderRadius?: number
|
|
91
|
+
borderTopLeftRadius?: number
|
|
92
|
+
borderTopRightRadius?: number
|
|
93
|
+
borderBottomLeftRadius?: number
|
|
94
|
+
borderBottomRightRadius?: number
|
|
95
|
+
borderWidth?: number
|
|
96
|
+
borderColor?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface TextStyle extends Style {
|
|
100
|
+
fontSize?: number
|
|
101
|
+
fontFamily?: string
|
|
102
|
+
fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
|
|
103
|
+
fontStyle?: 'normal' | 'italic'
|
|
104
|
+
color?: string
|
|
105
|
+
textAlign?: 'left' | 'center' | 'right'
|
|
106
|
+
lineHeight?: number
|
|
107
|
+
letterSpacing?: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface BaseProps {
|
|
111
|
+
name?: string
|
|
112
|
+
style?: Style
|
|
113
|
+
children?: React.ReactNode
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface TextProps extends Omit<BaseProps, 'style'> {
|
|
117
|
+
style?: TextStyle
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface StarProps extends BaseProps {
|
|
121
|
+
pointCount?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface PolygonProps extends BaseProps {
|
|
125
|
+
pointCount?: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface InstanceProps extends BaseProps {
|
|
129
|
+
componentId?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Component factory - creates intrinsic element wrapper
|
|
133
|
+
const c = <T extends BaseProps>(type: string): React.FC<T> =>
|
|
134
|
+
(props) => React.createElement(type, props)
|
|
135
|
+
|
|
136
|
+
// Components
|
|
137
|
+
export const Frame = c<BaseProps>('frame')
|
|
138
|
+
export const Rectangle = c<BaseProps>('rectangle')
|
|
139
|
+
export const Ellipse = c<BaseProps>('ellipse')
|
|
140
|
+
export const Text = c<TextProps>('text')
|
|
141
|
+
export const Line = c<BaseProps>('line')
|
|
142
|
+
export const Star = c<StarProps>('star')
|
|
143
|
+
export const Polygon = c<PolygonProps>('polygon')
|
|
144
|
+
export const Vector = c<BaseProps>('vector')
|
|
145
|
+
export const Component = c<BaseProps>('component')
|
|
146
|
+
export const Instance = c<InstanceProps>('instance')
|
|
147
|
+
export const Group = c<BaseProps>('group')
|
|
148
|
+
export const Page = c<BaseProps>('page')
|
|
149
|
+
export const View = Frame
|
|
150
|
+
|
|
151
|
+
// All component names for JSX transform
|
|
152
|
+
export const INTRINSIC_ELEMENTS = [
|
|
153
|
+
'Frame', 'Rectangle', 'Ellipse', 'Text', 'Line', 'Star',
|
|
154
|
+
'Polygon', 'Vector', 'Component', 'Instance', 'Group', 'Page', 'View'
|
|
155
|
+
] as const
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React → Figma Renderer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
renderToNodeChanges,
|
|
7
|
+
resetRenderedComponents,
|
|
8
|
+
getPendingComponentSetInstances,
|
|
9
|
+
clearPendingComponentSetInstances,
|
|
10
|
+
type RenderOptions,
|
|
11
|
+
type RenderResult,
|
|
12
|
+
type PendingComponentSetInstance,
|
|
13
|
+
} from './reconciler.ts'
|
|
14
|
+
export {
|
|
15
|
+
Frame,
|
|
16
|
+
Rectangle,
|
|
17
|
+
Ellipse,
|
|
18
|
+
Text,
|
|
19
|
+
Line,
|
|
20
|
+
Star,
|
|
21
|
+
Polygon,
|
|
22
|
+
Vector,
|
|
23
|
+
Component,
|
|
24
|
+
Instance,
|
|
25
|
+
Group,
|
|
26
|
+
Page,
|
|
27
|
+
View,
|
|
28
|
+
INTRINSIC_ELEMENTS,
|
|
29
|
+
// Variable bindings (StyleX-inspired)
|
|
30
|
+
defineVars,
|
|
31
|
+
figmaVar,
|
|
32
|
+
isVariable,
|
|
33
|
+
loadVariablesIntoRegistry,
|
|
34
|
+
isRegistryLoaded,
|
|
35
|
+
type FigmaVariable,
|
|
36
|
+
// Component definitions
|
|
37
|
+
defineComponent,
|
|
38
|
+
resetComponentRegistry,
|
|
39
|
+
getComponentRegistry,
|
|
40
|
+
} from './components.tsx'
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
// ComponentSet (variants)
|
|
44
|
+
defineComponentSet,
|
|
45
|
+
resetComponentSetRegistry,
|
|
46
|
+
getComponentSetRegistry,
|
|
47
|
+
} from './component-set.tsx'
|