Skip to content

Commit a5c79c8

Browse files
author
Andrey Okonetchnikov
committed
feat: Initial implementation
1 parent 233ee2f commit a5c79c8

File tree

8 files changed

+392
-0
lines changed

8 files changed

+392
-0
lines changed

src/.eslintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "plugin:react/recommended",
3+
"env": {
4+
"browser": true
5+
}
6+
}

src/File.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as React from "react";
2+
import { Query } from "react-apollo"
3+
import gql from "graphql-tag"
4+
5+
export const FigmaContext = React.createContext({
6+
fileId: null,
7+
pageName: null
8+
})
9+
10+
export const rectFragment = gql`
11+
fragment Rect on Node {
12+
position {
13+
x
14+
y
15+
}
16+
size {
17+
width
18+
height
19+
}
20+
}
21+
`
22+
23+
export const childrenFragment = gql`
24+
fragment ChildrenOfName on Frame {
25+
children(name: $nodeName) {
26+
... on Frame {
27+
id
28+
image(params: { format: "svg" })
29+
...Rect
30+
}
31+
... on Text {
32+
id
33+
name
34+
visible
35+
style {
36+
fontSize
37+
fontFamily
38+
fontWeight
39+
letterSpacing
40+
lineHeightPx
41+
}
42+
fill {
43+
r
44+
g
45+
b
46+
a
47+
}
48+
...Rect
49+
}
50+
}
51+
}
52+
53+
${rectFragment}
54+
`
55+
56+
const pageFragment = gql`
57+
fragment Page on File {
58+
lastModified
59+
pages(name: $pageName) {
60+
name
61+
frames {
62+
name
63+
...Rect
64+
...ChildrenOfName
65+
}
66+
}
67+
}
68+
`
69+
export const FIGMA_FILE_QUERY = gql`
70+
query FigmaFileQuery($fileId: ID!, $pageName: String!, $nodeName: String!) {
71+
file(id: $fileId) {
72+
...Page
73+
}
74+
}
75+
76+
${pageFragment}
77+
${childrenFragment}
78+
${rectFragment}
79+
`
80+
81+
const FIGMA_FILE_SUBSCRIPTION = gql`
82+
subscription onFigmaFileUpdated(
83+
$fileId: ID!
84+
$pageName: String!
85+
$nodeName: String!
86+
) {
87+
file(id: $fileId) {
88+
...Page
89+
}
90+
}
91+
92+
${pageFragment}
93+
${childrenFragment}
94+
${rectFragment}
95+
`
96+
97+
interface IFile {
98+
fileId: string,
99+
pageName: string,
100+
children?: any
101+
}
102+
103+
export default function File({ fileId, pageName, children }: IFile) {
104+
return (
105+
<Query
106+
query={FIGMA_FILE_QUERY}
107+
variables={{ fileId, pageName, nodeName: "" }}
108+
>
109+
{({ loading, data, error, subscribeToMore }) => {
110+
if (error) {
111+
console.error(error)
112+
return "Oh no!"
113+
} else if (loading) {
114+
return "Loading..."
115+
} else if (!data) {
116+
return null
117+
}
118+
const subscribeToFileUpdates = () =>
119+
subscribeToMore({
120+
document: FIGMA_FILE_SUBSCRIPTION,
121+
variables: { fileId, pageName, nodeName: "" },
122+
updateQuery: (prev, { subscriptionData }) => {
123+
console.log(subscriptionData)
124+
if (!subscriptionData.data) return prev
125+
const newFile = subscriptionData.data
126+
console.log("Figma file updated!")
127+
return newFile
128+
}
129+
})
130+
131+
subscribeToFileUpdates()
132+
133+
return (
134+
<FigmaContext.Provider
135+
value={{
136+
fileId,
137+
pageName
138+
}}
139+
>
140+
{children({ data })}
141+
</FigmaContext.Provider>
142+
)
143+
}}
144+
</Query>
145+
)
146+
}

src/Frame.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as React from "react"
2+
import styled from "styled-components"
3+
import Query from "./Query"
4+
5+
const NodeWrapper = styled("div")`
6+
position: relative;
7+
`
8+
9+
export interface INode {
10+
nodeName: string,
11+
children?: any
12+
}
13+
14+
export default function Frame({ nodeName, children }: INode) {
15+
return (
16+
<Query
17+
variables={{
18+
nodeName
19+
}}
20+
>
21+
{({ data }) => {
22+
const frame = data.file.pages[0].frames[0]
23+
const { size } = frame
24+
const { image } = frame.children[0]
25+
26+
return (
27+
<NodeWrapper
28+
css={{
29+
...size,
30+
background: `url(${image})`,
31+
backgroundSize: "cover"
32+
}}
33+
>
34+
{children}
35+
</NodeWrapper>
36+
)
37+
}}
38+
</Query>
39+
)
40+
}

src/Group.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from "react";
2+
import styled from "styled-components"
3+
import Query from "./Query"
4+
import { INode } from "./Frame";
5+
6+
const NodeWrapper = styled("div")`
7+
position: relative;
8+
`
9+
10+
export default function Group({ nodeName, children }: INode) {
11+
return (
12+
<Query
13+
variables={{
14+
nodeName
15+
}}
16+
>
17+
{({ data }) => {
18+
const frame = data.file.pages[0].frames[0]
19+
const { size, position } = frame.children[0]
20+
21+
return (
22+
<NodeWrapper
23+
css={{
24+
...size,
25+
top: position.y,
26+
left: position.x
27+
}}
28+
>
29+
{children}
30+
</NodeWrapper>
31+
)
32+
}}
33+
</Query>
34+
)
35+
}

src/Provider.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from "react";
2+
import ApolloClient from "apollo-client"
3+
import { ApolloProvider } from "react-apollo"
4+
import {
5+
InMemoryCache,
6+
IntrospectionFragmentMatcher
7+
} from "apollo-cache-inmemory"
8+
import { HttpLink } from "apollo-link-http"
9+
import { WebSocketLink } from "apollo-link-ws"
10+
import { split } from "apollo-link"
11+
import { getMainDefinition } from "apollo-utilities"
12+
// import { persistCache } from "apollo-cache-persist"
13+
import * as introspectionQueryResultData from "../fragmentTypes.json"
14+
15+
interface IProvider {
16+
host: string
17+
wsHost: string
18+
children?: any
19+
}
20+
21+
export default function Provider({ children, host, wsHost }: IProvider) {
22+
const fragmentMatcher = new IntrospectionFragmentMatcher({
23+
introspectionQueryResultData
24+
})
25+
26+
const cache = new InMemoryCache({ fragmentMatcher })
27+
28+
// persistCache({
29+
// cache,
30+
// storage: window.localStorage,
31+
// debug: true
32+
// })
33+
34+
const httpLink = new HttpLink({
35+
uri: `${host}/graphql`
36+
})
37+
38+
const wsLink = new WebSocketLink({
39+
uri: `${wsHost}/graphql`,
40+
options: {
41+
reconnect: true
42+
}
43+
})
44+
45+
const link = split(
46+
({ query }) => {
47+
const { kind, operation } = getMainDefinition(query)
48+
return kind === "OperationDefinition" && operation === "subscription"
49+
},
50+
wsLink,
51+
httpLink
52+
)
53+
54+
const client = new ApolloClient({
55+
link,
56+
cache
57+
})
58+
59+
return <ApolloProvider client={client}>{children}</ApolloProvider>
60+
}

src/Query.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from "react"
2+
import { Query as ApolloQuery } from "react-apollo"
3+
import { FigmaContext, FIGMA_FILE_QUERY } from "./File"
4+
5+
interface IQuery {
6+
variables: any
7+
children?: any
8+
}
9+
export default function Query({
10+
children,
11+
variables,
12+
}: IQuery) {
13+
return (
14+
<FigmaContext.Consumer>
15+
{({ fileId, pageName }) => (
16+
<ApolloQuery
17+
query={FIGMA_FILE_QUERY}
18+
variables={{ fileId, pageName, ...variables }}
19+
>
20+
{({ loading, data, error }) => {
21+
if (error) {
22+
console.error(error)
23+
return "Oh no!"
24+
} else if (loading) {
25+
return "Loading..."
26+
} else if (!data) {
27+
return null
28+
}
29+
return children({ data })
30+
}}
31+
</ApolloQuery>
32+
)}
33+
</FigmaContext.Consumer>
34+
)
35+
}

src/Text.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as React from "react";
2+
import styled from "styled-components"
3+
import { rgba } from "polished"
4+
import GoogleFontLoader from "react-google-font-loader"
5+
import Query from "./Query"
6+
import { INode } from "./Frame";
7+
8+
const NodeWrapper = styled("div")`
9+
position: absolute;
10+
`
11+
12+
export default function Text({ nodeName, children }:INode) {
13+
return (
14+
<Query
15+
variables={{
16+
nodeName
17+
}}
18+
>
19+
{({ data }) => {
20+
const theme = data.file.pages[0].frames[0]
21+
if (!theme.children.length) {
22+
console.warn(
23+
`No children returned from the query. Check if Figma file has a corresponding layer with name ${nodeName}`
24+
)
25+
return null
26+
}
27+
const { position, style, size, fill, visible } = theme.children[0]
28+
const { r, g, b, a } = fill
29+
const color = rgba(r * 255, g * 255, b * 255, a)
30+
const relativeX = position.x - theme.position.x
31+
const relativeY = position.y - theme.position.y
32+
const { fontFamily, fontWeight } = style
33+
34+
if (!visible) {
35+
return null
36+
}
37+
38+
return (
39+
<React.Fragment>
40+
<GoogleFontLoader
41+
fonts={[
42+
{
43+
font: fontFamily,
44+
weights: [fontWeight]
45+
}
46+
]}
47+
/>
48+
<NodeWrapper
49+
css={{
50+
...style,
51+
...size,
52+
lineHeight: `${style.lineHeightPx}px`,
53+
left: relativeX,
54+
top: relativeY,
55+
color
56+
}}
57+
>
58+
{children}
59+
</NodeWrapper>
60+
</React.Fragment>
61+
)
62+
}}
63+
</Query>
64+
)
65+
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as File } from "./File"
2+
export { default as Frame } from "./Frame"
3+
export { default as Group } from "./Group"
4+
export { default as Text } from "./Text"
5+
export { default as Provider } from "./Provider"

0 commit comments

Comments
 (0)